From 7b19177c8ae9e2edfe2edf1bf520389c4f400a30 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 10 Dec 2025 10:40:36 +0900 Subject: [PATCH] Revert "fix(hooks): improve TODO continuation race condition handling with state machine pattern" This reverts commit e59b0be6cc380a3750e2d56c4c7ba553feef2c40. --- src/hooks/todo-continuation-enforcer.ts | 97 +++++++++++-------------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/src/hooks/todo-continuation-enforcer.ts b/src/hooks/todo-continuation-enforcer.ts index b542cde..9c6f9c9 100644 --- a/src/hooks/todo-continuation-enforcer.ts +++ b/src/hooks/todo-continuation-enforcer.ts @@ -7,8 +7,6 @@ interface Todo { id: string } -type SessionStatus = "idle" | "aborted" | "continuation-sent" - const CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION] Incomplete tasks remain in your todo list. Continue working on the next pending task. @@ -17,8 +15,6 @@ Incomplete tasks remain in your todo list. Continue working on the next pending - Mark each task complete when finished - Do not stop until all tasks are done` -const CONTINUATION_DELAY_MS = 500 - function detectInterrupt(error: unknown): boolean { if (!error) return false if (typeof error === "object") { @@ -37,36 +33,43 @@ function detectInterrupt(error: unknown): boolean { } export function createTodoContinuationEnforcer(ctx: PluginInput) { - const sessionStates = new Map() - const pendingContinuations = new Map() + const remindedSessions = new Set() + const interruptedSessions = new Set() + const errorSessions = new Set() - const cleanupSession = (sessionID: string) => { - const timer = pendingContinuations.get(sessionID) - if (timer) clearTimeout(timer) - pendingContinuations.delete(sessionID) - sessionStates.delete(sessionID) - } + return async ({ event }: { event: { type: string; properties?: unknown } }) => { + const props = event.properties as Record | undefined - const cancelPendingContinuation = (sessionID: string) => { - const timer = pendingContinuations.get(sessionID) - if (timer) { - clearTimeout(timer) - pendingContinuations.delete(sessionID) + if (event.type === "session.error") { + const sessionID = props?.sessionID as string | undefined + if (sessionID) { + errorSessions.add(sessionID) + if (detectInterrupt(props?.error)) { + interruptedSessions.add(sessionID) + } + } + return } - sessionStates.set(sessionID, "aborted") - } - const scheduleContinuation = (sessionID: string) => { - const prev = pendingContinuations.get(sessionID) - if (prev) clearTimeout(prev) + if (event.type === "session.idle") { + const sessionID = props?.sessionID as string | undefined + if (!sessionID) return - sessionStates.set(sessionID, "idle") + // Wait for potential session.error events to be processed first + await new Promise(resolve => setTimeout(resolve, 150)) - const timer = setTimeout(async () => { - pendingContinuations.delete(sessionID) + const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID) + + interruptedSessions.delete(sessionID) + errorSessions.delete(sessionID) - const state = sessionStates.get(sessionID) - if (state !== "idle") return + if (shouldBypass) { + return + } + + if (remindedSessions.has(sessionID)) { + return + } let todos: Todo[] = [] try { @@ -90,7 +93,13 @@ export function createTodoContinuationEnforcer(ctx: PluginInput) { return } - sessionStates.set(sessionID, "continuation-sent") + remindedSessions.add(sessionID) + + // Re-check if abort occurred during the delay + if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) { + remindedSessions.delete(sessionID) + return + } try { await ctx.client.session.prompt({ @@ -106,46 +115,24 @@ export function createTodoContinuationEnforcer(ctx: PluginInput) { query: { directory: ctx.directory }, }) } catch { - sessionStates.delete(sessionID) + remindedSessions.delete(sessionID) } - }, CONTINUATION_DELAY_MS) - - pendingContinuations.set(sessionID, timer) - } - - return async ({ event }: { event: { type: string; properties?: unknown } }) => { - const props = event.properties as Record | undefined - - if (event.type === "session.error") { - const sessionID = props?.sessionID as string | undefined - if (sessionID && detectInterrupt(props?.error)) { - cancelPendingContinuation(sessionID) - } - return - } - - if (event.type === "session.idle") { - const sessionID = props?.sessionID as string | undefined - if (!sessionID) return - - const state = sessionStates.get(sessionID) - if (state === "continuation-sent") return - - scheduleContinuation(sessionID) } if (event.type === "message.updated") { const info = props?.info as Record | undefined const sessionID = info?.sessionID as string | undefined if (sessionID && info?.role === "user") { - sessionStates.delete(sessionID) + remindedSessions.delete(sessionID) } } if (event.type === "session.deleted") { const sessionInfo = props?.info as { id?: string } | undefined if (sessionInfo?.id) { - cleanupSession(sessionInfo.id) + remindedSessions.delete(sessionInfo.id) + interruptedSessions.delete(sessionInfo.id) + errorSessions.delete(sessionInfo.id) } } }