fix(todo-continuation): remove activity-based stagnation bypass
Activity signals (tool calls like compress, grep, bash) were treated as 'progress' by the stagnation detector, resetting the stagnation counter every cycle. This prevented MAX_STAGNATION_COUNT from being reached, causing infinite continuation loops when models degrade to minimal responses in long sessions (e.g. GLM-5.1 at ~100K tokens). Stagnation now only tracks actual todo state changes: incomplete count decrease, completed count increase, or todo snapshot change. Tool-level activity no longer resets the stagnation counter.
This commit is contained in:
committed by
YeonGyu-Kim
parent
fe66c96215
commit
68e9d54fa5
@@ -28,7 +28,6 @@ function createStateStore(): {
|
||||
getState: () => state,
|
||||
getExistingState: () => state,
|
||||
startPruneInterval: () => {},
|
||||
recordActivity: () => {},
|
||||
trackContinuationProgress: () => progressUpdate,
|
||||
resetContinuationProgress: (sessionID: string) => {
|
||||
resetCalls.push(sessionID)
|
||||
|
||||
@@ -16,14 +16,6 @@ import { acknowledgeCompactionGuard, isCompactionGuardActive } from "./compactio
|
||||
import type { SessionStateStore } from "./session-state"
|
||||
import { startCountdown } from "./countdown"
|
||||
|
||||
function shouldAllowActivityProgress(modelID: string | undefined): boolean {
|
||||
if (!modelID) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !modelID.toLowerCase().includes("codex")
|
||||
}
|
||||
|
||||
export async function handleSessionIdle(args: {
|
||||
ctx: PluginInput
|
||||
sessionID: string
|
||||
@@ -204,7 +196,6 @@ export async function handleSessionIdle(args: {
|
||||
sessionID,
|
||||
incompleteCount,
|
||||
todos,
|
||||
{ allowActivityProgress: shouldAllowActivityProgress(resolvedInfo?.model?.modelID) },
|
||||
)
|
||||
if (shouldStopForStagnation({ sessionID, incompleteCount, progressUpdate })) {
|
||||
return
|
||||
|
||||
@@ -30,7 +30,6 @@ export function handleNonIdleEvent(args: {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
state.tokenLimitDetected = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
return
|
||||
@@ -41,7 +40,6 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
return
|
||||
@@ -57,7 +55,6 @@ export function handleNonIdleEvent(args: {
|
||||
const state = sessionStateStore.getExistingState(targetSessionID)
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
sessionStateStore.recordActivity(targetSessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(targetSessionID)
|
||||
}
|
||||
@@ -71,7 +68,6 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
}
|
||||
@@ -85,7 +81,6 @@ export function handleNonIdleEvent(args: {
|
||||
if (state) {
|
||||
state.abortDetectedAt = undefined
|
||||
state.wasCancelled = false
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
}
|
||||
sessionStateStore.cancelCountdown(sessionID)
|
||||
}
|
||||
|
||||
@@ -144,9 +144,9 @@ describe("createSessionStateStore", () => {
|
||||
expect(stagnatedAgainUpdate.stagnationCount).toBe(1)
|
||||
})
|
||||
|
||||
test("given non-codex activity happens after a successful continuation, treats it as progress", () => {
|
||||
test("given tool activity happens after a successful continuation without todo changes, keeps counting stagnation", () => {
|
||||
// given
|
||||
const sessionID = "ses-non-codex-activity-progress"
|
||||
const sessionID = "ses-activity-stagnation"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
const todos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
@@ -154,40 +154,12 @@ describe("createSessionStateStore", () => {
|
||||
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 1, todos)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
|
||||
// when
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(
|
||||
sessionID,
|
||||
1,
|
||||
todos,
|
||||
{ allowActivityProgress: true },
|
||||
)
|
||||
|
||||
// then
|
||||
expect(progressUpdate.hasProgressed).toBe(true)
|
||||
expect(progressUpdate.progressSource).toBe("activity")
|
||||
expect(progressUpdate.stagnationCount).toBe(0)
|
||||
})
|
||||
|
||||
test("given codex activity happens after a successful continuation, keeps counting stagnation", () => {
|
||||
// given
|
||||
const sessionID = "ses-codex-activity-stagnation"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
const todos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
]
|
||||
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 1, todos)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
sessionStateStore.recordActivity(sessionID)
|
||||
|
||||
// when
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(
|
||||
sessionID,
|
||||
1,
|
||||
todos,
|
||||
{ allowActivityProgress: false },
|
||||
)
|
||||
|
||||
// then
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ContinuationProgressOptions, SessionState, Todo } from "./types"
|
||||
import type { SessionState, Todo } from "./types"
|
||||
|
||||
type TimerHandle = number | { unref?: () => void }
|
||||
|
||||
@@ -16,8 +16,6 @@ interface TrackedSessionState {
|
||||
lastAccessedAt: number
|
||||
lastCompletedCount?: number
|
||||
lastTodoSnapshot?: string
|
||||
activitySignalCount: number
|
||||
lastObservedActivitySignalCount?: number
|
||||
}
|
||||
|
||||
export interface ContinuationProgressUpdate {
|
||||
@@ -25,19 +23,17 @@ export interface ContinuationProgressUpdate {
|
||||
previousStagnationCount: number
|
||||
stagnationCount: number
|
||||
hasProgressed: boolean
|
||||
progressSource: "none" | "todo" | "activity"
|
||||
progressSource: "none" | "todo"
|
||||
}
|
||||
|
||||
export interface SessionStateStore {
|
||||
getState: (sessionID: string) => SessionState
|
||||
getExistingState: (sessionID: string) => SessionState | undefined
|
||||
startPruneInterval: () => void
|
||||
recordActivity: (sessionID: string) => void
|
||||
trackContinuationProgress: (
|
||||
sessionID: string,
|
||||
incompleteCount: number,
|
||||
todos?: Todo[],
|
||||
options?: ContinuationProgressOptions,
|
||||
) => ContinuationProgressUpdate
|
||||
resetContinuationProgress: (sessionID: string) => void
|
||||
cancelCountdown: (sessionID: string) => void
|
||||
@@ -113,7 +109,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
const trackedSession: TrackedSessionState = {
|
||||
state: rawState,
|
||||
lastAccessedAt: Date.now(),
|
||||
activitySignalCount: 0,
|
||||
}
|
||||
sessions.set(sessionID, trackedSession)
|
||||
return trackedSession
|
||||
@@ -132,16 +127,10 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function recordActivity(sessionID: string): void {
|
||||
const trackedSession = getTrackedSession(sessionID)
|
||||
trackedSession.activitySignalCount += 1
|
||||
}
|
||||
|
||||
function trackContinuationProgress(
|
||||
sessionID: string,
|
||||
incompleteCount: number,
|
||||
todos?: Todo[],
|
||||
options: ContinuationProgressOptions = {},
|
||||
): ContinuationProgressUpdate {
|
||||
const trackedSession = getTrackedSession(sessionID)
|
||||
const state = trackedSession.state
|
||||
@@ -149,7 +138,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
const previousStagnationCount = state.stagnationCount
|
||||
const currentCompletedCount = todos?.filter((todo) => todo.status === "completed").length
|
||||
const currentTodoSnapshot = todos ? getTodoSnapshot(todos) : undefined
|
||||
const currentActivitySignalCount = trackedSession.activitySignalCount
|
||||
const hasCompletedMoreTodos =
|
||||
currentCompletedCount !== undefined
|
||||
&& trackedSession.lastCompletedCount !== undefined
|
||||
@@ -158,10 +146,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
currentTodoSnapshot !== undefined
|
||||
&& trackedSession.lastTodoSnapshot !== undefined
|
||||
&& currentTodoSnapshot !== trackedSession.lastTodoSnapshot
|
||||
const hasObservedExternalActivity =
|
||||
options.allowActivityProgress === true
|
||||
&& trackedSession.lastObservedActivitySignalCount !== undefined
|
||||
&& currentActivitySignalCount > trackedSession.lastObservedActivitySignalCount
|
||||
const hadSuccessfulInjectionAwaitingProgressCheck = state.awaitingPostInjectionProgressCheck === true
|
||||
|
||||
state.lastIncompleteCount = incompleteCount
|
||||
@@ -171,7 +155,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
if (currentTodoSnapshot !== undefined) {
|
||||
trackedSession.lastTodoSnapshot = currentTodoSnapshot
|
||||
}
|
||||
trackedSession.lastObservedActivitySignalCount = currentActivitySignalCount
|
||||
|
||||
if (previousIncompleteCount === undefined) {
|
||||
state.stagnationCount = 0
|
||||
@@ -184,13 +167,9 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
}
|
||||
}
|
||||
|
||||
const progressSource = incompleteCount < previousIncompleteCount || hasCompletedMoreTodos || hasTodoSnapshotChanged
|
||||
? "todo"
|
||||
: hasObservedExternalActivity
|
||||
? "activity"
|
||||
: "none"
|
||||
const hasProgressed = incompleteCount < previousIncompleteCount || hasCompletedMoreTodos || hasTodoSnapshotChanged
|
||||
|
||||
if (progressSource !== "none") {
|
||||
if (hasProgressed) {
|
||||
state.stagnationCount = 0
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
return {
|
||||
@@ -198,7 +177,7 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
previousStagnationCount,
|
||||
stagnationCount: state.stagnationCount,
|
||||
hasProgressed: true,
|
||||
progressSource,
|
||||
progressSource: "todo",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +215,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
trackedSession.lastCompletedCount = undefined
|
||||
trackedSession.lastTodoSnapshot = undefined
|
||||
trackedSession.activitySignalCount = 0
|
||||
trackedSession.lastObservedActivitySignalCount = undefined
|
||||
}
|
||||
|
||||
function cancelCountdown(sessionID: string): void {
|
||||
@@ -282,7 +259,6 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
getState,
|
||||
getExistingState,
|
||||
startPruneInterval,
|
||||
recordActivity,
|
||||
trackContinuationProgress,
|
||||
resetContinuationProgress,
|
||||
cancelCountdown,
|
||||
|
||||
@@ -68,7 +68,3 @@ export interface ResolveLatestMessageInfoResult {
|
||||
encounteredCompaction: boolean
|
||||
latestMessageWasCompaction: boolean
|
||||
}
|
||||
|
||||
export interface ContinuationProgressOptions {
|
||||
allowActivityProgress?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user