fix(todo-continuation-enforcer): re-verify todos after countdown to prevent stale data injection (#239)

Fixes the race condition where the todo continuation hook would inject a
continuation prompt even when all todos had been completed during the
countdown period.

The root cause was that executeAfterCountdown() used stale todo data from
the initial session.idle check without re-verifying that incomplete todos
still existed after the countdown finished.

Changes:
- Add fresh todo verification in executeAfterCountdown() before prompt injection
- Use fresh todo data in the continuation prompt message
- Abort injection if no incomplete todos remain after countdown

This properly handles the case where:
1. session.idle fires (e.g., user enters shell mode in TUI)
2. Initial check finds incomplete todos, starts countdown
3. During countdown, todos get completed
4. Countdown ends, fresh check detects no incomplete todos
5. Hook aborts instead of injecting stale prompt

Fixes #234

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
Sisyphus
2025-12-25 22:14:02 +09:00
committed by GitHub
parent d9cfc1ec97
commit 9bfe7d8a1d

View File

@@ -215,6 +215,30 @@ export function createTodoContinuationEnforcer(
return return
} }
let freshTodos: Todo[] = []
try {
log(`[${HOOK_NAME}] Re-verifying todos after countdown`, { sessionID })
const response = await ctx.client.session.todo({
path: { id: sessionID },
})
freshTodos = (response.data ?? response) as Todo[]
log(`[${HOOK_NAME}] Fresh todo count`, { sessionID, todosCount: freshTodos?.length ?? 0 })
} catch (err) {
log(`[${HOOK_NAME}] Failed to re-verify todos`, { sessionID, error: String(err) })
return
}
const freshIncomplete = freshTodos.filter(
(t) => t.status !== "completed" && t.status !== "cancelled"
)
if (freshIncomplete.length === 0) {
log(`[${HOOK_NAME}] Abort: no incomplete todos after countdown`, { sessionID, total: freshTodos.length })
return
}
log(`[${HOOK_NAME}] Confirmed incomplete todos, proceeding with injection`, { sessionID, incomplete: freshIncomplete.length, total: freshTodos.length })
remindedSessions.add(sessionID) remindedSessions.add(sessionID)
try { try {
@@ -237,7 +261,7 @@ export function createTodoContinuationEnforcer(
parts: [ parts: [
{ {
type: "text", type: "text",
text: `${CONTINUATION_PROMPT}\n\n[Status: ${todos.length - incomplete.length}/${todos.length} completed, ${incomplete.length} remaining]`, text: `${CONTINUATION_PROMPT}\n\n[Status: ${freshTodos.length - freshIncomplete.length}/${freshTodos.length} completed, ${freshIncomplete.length} remaining]`,
}, },
], ],
}, },