From 9fa9dace2c772e41fedef7ef51fa6ac0d121ed75 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 19 Feb 2026 14:07:52 +0900 Subject: [PATCH 1/2] fix(todo-continuation-enforcer): check isContinuationStopped in injectContinuation to close race window When /stop-continuation is invoked during the 2s countdown, the stop flag was never checked inside injectContinuation, so the injection would still fire after the countdown elapsed. Propagate isContinuationStopped from handleSessionIdle through startCountdown into injectContinuation, where it is now re-checked before any API call. --- .../todo-continuation-enforcer/continuation-injection.ts | 7 +++++++ src/hooks/todo-continuation-enforcer/countdown.ts | 3 +++ src/hooks/todo-continuation-enforcer/idle-event.ts | 1 + 3 files changed, 11 insertions(+) diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.ts index b48770ab..ba60d23b 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.ts @@ -37,6 +37,7 @@ export async function injectContinuation(args: { skipAgents?: string[] resolvedInfo?: ResolvedMessageInfo sessionStateStore: SessionStateStore + isContinuationStopped?: (sessionID: string) => boolean }): Promise { const { ctx, @@ -45,6 +46,7 @@ export async function injectContinuation(args: { skipAgents = DEFAULT_SKIP_AGENTS, resolvedInfo, sessionStateStore, + isContinuationStopped, } = args const state = sessionStateStore.getExistingState(sessionID) @@ -53,6 +55,11 @@ export async function injectContinuation(args: { return } + if (isContinuationStopped?.(sessionID)) { + log(`[${HOOK_NAME}] Skipped injection: continuation stopped for session`, { sessionID }) + return + } + const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task: { status: string }) => task.status === "running") : false diff --git a/src/hooks/todo-continuation-enforcer/countdown.ts b/src/hooks/todo-continuation-enforcer/countdown.ts index 7404d32d..007e7dc3 100644 --- a/src/hooks/todo-continuation-enforcer/countdown.ts +++ b/src/hooks/todo-continuation-enforcer/countdown.ts @@ -38,6 +38,7 @@ export function startCountdown(args: { backgroundManager?: BackgroundManager skipAgents: string[] sessionStateStore: SessionStateStore + isContinuationStopped?: (sessionID: string) => boolean }): void { const { ctx, @@ -47,6 +48,7 @@ export function startCountdown(args: { backgroundManager, skipAgents, sessionStateStore, + isContinuationStopped, } = args const state = sessionStateStore.getState(sessionID) @@ -72,6 +74,7 @@ export function startCountdown(args: { skipAgents, resolvedInfo, sessionStateStore, + isContinuationStopped, }) }, COUNTDOWN_SECONDS * 1000) diff --git a/src/hooks/todo-continuation-enforcer/idle-event.ts b/src/hooks/todo-continuation-enforcer/idle-event.ts index 689672c0..10708d1a 100644 --- a/src/hooks/todo-continuation-enforcer/idle-event.ts +++ b/src/hooks/todo-continuation-enforcer/idle-event.ts @@ -187,5 +187,6 @@ export async function handleSessionIdle(args: { backgroundManager, skipAgents, sessionStateStore, + isContinuationStopped, }) } From a551fceca9bd5e8bb3cc8eb9936652bd9468b647 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 19 Feb 2026 14:08:03 +0900 Subject: [PATCH 2/2] test(todo-continuation-enforcer): cover isContinuationStopped race during countdown Adds a regression test for the race where /stop-continuation fires after handleSessionIdle passes the flag check but before injectContinuation runs. Verifies no injection occurs when the flag becomes true mid-countdown. --- .../todo-continuation-enforcer.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/hooks/todo-continuation-enforcer/todo-continuation-enforcer.test.ts b/src/hooks/todo-continuation-enforcer/todo-continuation-enforcer.test.ts index 10f23ff3..defe8c8b 100644 --- a/src/hooks/todo-continuation-enforcer/todo-continuation-enforcer.test.ts +++ b/src/hooks/todo-continuation-enforcer/todo-continuation-enforcer.test.ts @@ -1608,6 +1608,31 @@ describe("todo-continuation-enforcer", () => { expect(promptCalls).toHaveLength(0) }) + test("should not inject when isContinuationStopped becomes true during countdown", async () => { + // given - session where continuation is not stopped at idle time but stops during countdown + const sessionID = "main-race-condition" + setMainSession(sessionID) + let stopped = false + + const hook = createTodoContinuationEnforcer(createMockPluginInput(), { + isContinuationStopped: () => stopped, + }) + + // when - session goes idle with continuation not yet stopped + await hook.handler({ + event: { type: "session.idle", properties: { sessionID } }, + }) + + // when - stop-continuation fires during the 2s countdown window + stopped = true + + // when - countdown elapses and injectContinuation fires + await fakeTimers.advanceBy(3000) + + // then - no injection because isContinuationStopped became true before injectContinuation ran + expect(promptCalls).toHaveLength(0) + }) + test("should inject when isContinuationStopped returns false", async () => { fakeTimers.restore() // given - session with continuation not stopped