fix(todo-continuation-enforcer): stop looping after all todos complete (#4013)
Two P0 fixes for the assistant loop that repeats its final summary 3-5 times
after all todos are marked completed before stagnation detection finally halts it.
P0.1 — session-level stop flag: when handleSessionIdle detects incompleteCount===0
it now sets state.allTodosCompletedAt. Subsequent idle events for the same session
bail out immediately at the top of the function before any HTTP fetch or injection
logic runs, preventing the re-entry loop regardless of todo-fetch caching latency.
The flag is cleared by resetContinuationProgress so sessions that receive new todos
after completion resume enforcement normally.
P0.2 — snapshot comparison scope: getTodoSnapshot now only serialises the
{id → status} mapping (sorted by key). Content and priority changes are excluded
from the comparison. Previously those fields were included, causing hasTodoSnapshotChanged
to return true whenever the LLM re-wrote todo text with identical status — which
reported progressSource="todo" and reset stagnationCount to 0, preventing
MAX_STAGNATION_COUNT=3 from ever being reached.
P1 fixes (CONTINUATION_PROMPT adversarial wording, 10 s completion grace period)
are deferred to a follow-up PR as noted in the issue.
This commit is contained in:
@@ -43,29 +43,17 @@ export interface SessionStateStore {
|
||||
}
|
||||
|
||||
function getTodoSnapshot(todos: Todo[]): string {
|
||||
const normalizedTodos = todos
|
||||
// Only compare {id → status} mappings. Content/priority changes do not represent
|
||||
// meaningful progress and must not reset the stagnation counter (issue #4013 P0.2).
|
||||
const entries = todos
|
||||
.map((todo) => ({
|
||||
id: todo.id ?? null,
|
||||
content: todo.content,
|
||||
priority: todo.priority,
|
||||
key: todo.id ?? `${todo.content}:${todo.priority}`,
|
||||
status: todo.status,
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const leftKey = left.id ?? `${left.content}:${left.priority}:${left.status}`
|
||||
const rightKey = right.id ?? `${right.content}:${right.priority}:${right.status}`
|
||||
if (leftKey !== rightKey) {
|
||||
return leftKey.localeCompare(rightKey)
|
||||
}
|
||||
if (left.content !== right.content) {
|
||||
return left.content.localeCompare(right.content)
|
||||
}
|
||||
if (left.priority !== right.priority) {
|
||||
return left.priority.localeCompare(right.priority)
|
||||
}
|
||||
return left.status.localeCompare(right.status)
|
||||
})
|
||||
.sort((left, right) => left.key.localeCompare(right.key))
|
||||
.map(({ key, status }) => `${key}=${status}`)
|
||||
|
||||
return JSON.stringify(normalizedTodos)
|
||||
return entries.join("|")
|
||||
}
|
||||
|
||||
export function createSessionStateStore(): SessionStateStore {
|
||||
@@ -213,6 +201,7 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
state.lastIncompleteCount = undefined
|
||||
state.stagnationCount = 0
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
state.allTodosCompletedAt = undefined
|
||||
trackedSession.lastCompletedCount = undefined
|
||||
trackedSession.lastTodoSnapshot = undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user