fix(todo-continuation-enforcer): add 500ms grace period to prevent false countdown cancellation (#424)
This commit is contained in:
@@ -216,7 +216,7 @@ describe("todo-continuation-enforcer", () => {
|
|||||||
expect(promptCalls.length).toBe(1)
|
expect(promptCalls.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should cancel countdown on user message", async () => {
|
test("should cancel countdown on user message after grace period", async () => {
|
||||||
// #given - session starting countdown
|
// #given - session starting countdown
|
||||||
const sessionID = "main-cancel"
|
const sessionID = "main-cancel"
|
||||||
setMainSession(sessionID)
|
setMainSession(sessionID)
|
||||||
@@ -228,7 +228,8 @@ describe("todo-continuation-enforcer", () => {
|
|||||||
event: { type: "session.idle", properties: { sessionID } },
|
event: { type: "session.idle", properties: { sessionID } },
|
||||||
})
|
})
|
||||||
|
|
||||||
// #when - user sends message immediately (before 2s countdown)
|
// #when - wait past grace period (500ms), then user sends message
|
||||||
|
await new Promise(r => setTimeout(r, 600))
|
||||||
await hook.handler({
|
await hook.handler({
|
||||||
event: {
|
event: {
|
||||||
type: "message.updated",
|
type: "message.updated",
|
||||||
@@ -236,11 +237,37 @@ describe("todo-continuation-enforcer", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// #then - wait past countdown time and verify no injection
|
// #then - wait past countdown time and verify no injection (countdown was cancelled)
|
||||||
await new Promise(r => setTimeout(r, 2500))
|
await new Promise(r => setTimeout(r, 2500))
|
||||||
expect(promptCalls).toHaveLength(0)
|
expect(promptCalls).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should ignore user message within grace period", async () => {
|
||||||
|
// #given - session starting countdown
|
||||||
|
const sessionID = "main-grace"
|
||||||
|
setMainSession(sessionID)
|
||||||
|
|
||||||
|
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||||
|
|
||||||
|
// #when - session goes idle
|
||||||
|
await hook.handler({
|
||||||
|
event: { type: "session.idle", properties: { sessionID } },
|
||||||
|
})
|
||||||
|
|
||||||
|
// #when - user message arrives within grace period (immediately)
|
||||||
|
await hook.handler({
|
||||||
|
event: {
|
||||||
|
type: "message.updated",
|
||||||
|
properties: { info: { sessionID, role: "user" } }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #then - countdown should continue (message was ignored)
|
||||||
|
// wait past 2s countdown and verify injection happens
|
||||||
|
await new Promise(r => setTimeout(r, 2500))
|
||||||
|
expect(promptCalls).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
test("should cancel countdown on assistant activity", async () => {
|
test("should cancel countdown on assistant activity", async () => {
|
||||||
// #given - session starting countdown
|
// #given - session starting countdown
|
||||||
const sessionID = "main-assistant"
|
const sessionID = "main-assistant"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface SessionState {
|
|||||||
countdownTimer?: ReturnType<typeof setTimeout>
|
countdownTimer?: ReturnType<typeof setTimeout>
|
||||||
countdownInterval?: ReturnType<typeof setInterval>
|
countdownInterval?: ReturnType<typeof setInterval>
|
||||||
isRecovering?: boolean
|
isRecovering?: boolean
|
||||||
|
countdownStartedAt?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION]
|
const CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION]
|
||||||
@@ -45,6 +46,7 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
|
|||||||
|
|
||||||
const COUNTDOWN_SECONDS = 2
|
const COUNTDOWN_SECONDS = 2
|
||||||
const TOAST_DURATION_MS = 900
|
const TOAST_DURATION_MS = 900
|
||||||
|
const COUNTDOWN_GRACE_PERIOD_MS = 500
|
||||||
|
|
||||||
function getMessageDir(sessionID: string): string | null {
|
function getMessageDir(sessionID: string): string | null {
|
||||||
if (!existsSync(MESSAGE_STORAGE)) return null
|
if (!existsSync(MESSAGE_STORAGE)) return null
|
||||||
@@ -113,6 +115,7 @@ export function createTodoContinuationEnforcer(
|
|||||||
clearInterval(state.countdownInterval)
|
clearInterval(state.countdownInterval)
|
||||||
state.countdownInterval = undefined
|
state.countdownInterval = undefined
|
||||||
}
|
}
|
||||||
|
state.countdownStartedAt = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup(sessionID: string): void {
|
function cleanup(sessionID: string): void {
|
||||||
@@ -228,6 +231,7 @@ export function createTodoContinuationEnforcer(
|
|||||||
|
|
||||||
let secondsRemaining = COUNTDOWN_SECONDS
|
let secondsRemaining = COUNTDOWN_SECONDS
|
||||||
showCountdownToast(secondsRemaining, incompleteCount)
|
showCountdownToast(secondsRemaining, incompleteCount)
|
||||||
|
state.countdownStartedAt = Date.now()
|
||||||
|
|
||||||
state.countdownInterval = setInterval(() => {
|
state.countdownInterval = setInterval(() => {
|
||||||
secondsRemaining--
|
secondsRemaining--
|
||||||
@@ -334,6 +338,13 @@ export function createTodoContinuationEnforcer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
|
if (state?.countdownStartedAt) {
|
||||||
|
const elapsed = Date.now() - state.countdownStartedAt
|
||||||
|
if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
|
||||||
|
log(`[${HOOK_NAME}] Ignoring user message in grace period`, { sessionID, elapsed })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
cancelCountdown(sessionID)
|
cancelCountdown(sessionID)
|
||||||
log(`[${HOOK_NAME}] User message: cleared abort state`, { sessionID })
|
log(`[${HOOK_NAME}] User message: cleared abort state`, { sessionID })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user