Skip to content

Commit 8b99133

Browse files
authored
fix(todo-continuation-enforcer): add 500ms grace period to prevent false countdown cancellation (#424)
1 parent fa204d8 commit 8b99133

File tree

2 files changed

+41
-3
lines changed

2 files changed

+41
-3
lines changed

src/hooks/todo-continuation-enforcer.test.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ describe("todo-continuation-enforcer", () => {
216216
expect(promptCalls.length).toBe(1)
217217
})
218218

219-
test("should cancel countdown on user message", async () => {
219+
test("should cancel countdown on user message after grace period", async () => {
220220
// #given - session starting countdown
221221
const sessionID = "main-cancel"
222222
setMainSession(sessionID)
@@ -228,19 +228,46 @@ describe("todo-continuation-enforcer", () => {
228228
event: { type: "session.idle", properties: { sessionID } },
229229
})
230230

231-
// #when - user sends message immediately (before 2s countdown)
231+
// #when - wait past grace period (500ms), then user sends message
232+
await new Promise(r => setTimeout(r, 600))
232233
await hook.handler({
233234
event: {
234235
type: "message.updated",
235236
properties: { info: { sessionID, role: "user" } }
236237
},
237238
})
238239

239-
// #then - wait past countdown time and verify no injection
240+
// #then - wait past countdown time and verify no injection (countdown was cancelled)
240241
await new Promise(r => setTimeout(r, 2500))
241242
expect(promptCalls).toHaveLength(0)
242243
})
243244

245+
test("should ignore user message within grace period", async () => {
246+
// #given - session starting countdown
247+
const sessionID = "main-grace"
248+
setMainSession(sessionID)
249+
250+
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
251+
252+
// #when - session goes idle
253+
await hook.handler({
254+
event: { type: "session.idle", properties: { sessionID } },
255+
})
256+
257+
// #when - user message arrives within grace period (immediately)
258+
await hook.handler({
259+
event: {
260+
type: "message.updated",
261+
properties: { info: { sessionID, role: "user" } }
262+
},
263+
})
264+
265+
// #then - countdown should continue (message was ignored)
266+
// wait past 2s countdown and verify injection happens
267+
await new Promise(r => setTimeout(r, 2500))
268+
expect(promptCalls).toHaveLength(1)
269+
})
270+
244271
test("should cancel countdown on assistant activity", async () => {
245272
// #given - session starting countdown
246273
const sessionID = "main-assistant"

src/hooks/todo-continuation-enforcer.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface SessionState {
3333
countdownTimer?: ReturnType<typeof setTimeout>
3434
countdownInterval?: ReturnType<typeof setInterval>
3535
isRecovering?: boolean
36+
countdownStartedAt?: number
3637
}
3738

3839
const CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION]
@@ -45,6 +46,7 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
4546

4647
const COUNTDOWN_SECONDS = 2
4748
const TOAST_DURATION_MS = 900
49+
const COUNTDOWN_GRACE_PERIOD_MS = 500
4850

4951
function getMessageDir(sessionID: string): string | null {
5052
if (!existsSync(MESSAGE_STORAGE)) return null
@@ -113,6 +115,7 @@ export function createTodoContinuationEnforcer(
113115
clearInterval(state.countdownInterval)
114116
state.countdownInterval = undefined
115117
}
118+
state.countdownStartedAt = undefined
116119
}
117120

118121
function cleanup(sessionID: string): void {
@@ -228,6 +231,7 @@ export function createTodoContinuationEnforcer(
228231

229232
let secondsRemaining = COUNTDOWN_SECONDS
230233
showCountdownToast(secondsRemaining, incompleteCount)
234+
state.countdownStartedAt = Date.now()
231235

232236
state.countdownInterval = setInterval(() => {
233237
secondsRemaining--
@@ -334,6 +338,13 @@ export function createTodoContinuationEnforcer(
334338
}
335339

336340
if (role === "user") {
341+
if (state?.countdownStartedAt) {
342+
const elapsed = Date.now() - state.countdownStartedAt
343+
if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
344+
log(`[${HOOK_NAME}] Ignoring user message in grace period`, { sessionID, elapsed })
345+
return
346+
}
347+
}
337348
cancelCountdown(sessionID)
338349
log(`[${HOOK_NAME}] User message: cleared abort state`, { sessionID })
339350
}

0 commit comments

Comments
 (0)