diff --git a/src/hooks/todo-continuation-enforcer/non-idle-events.test.ts b/src/hooks/todo-continuation-enforcer/non-idle-events.test.ts index b0030118d..ef23a761b 100644 --- a/src/hooks/todo-continuation-enforcer/non-idle-events.test.ts +++ b/src/hooks/todo-continuation-enforcer/non-idle-events.test.ts @@ -67,4 +67,35 @@ describe("handleNonIdleEvent", () => { expect(state.wasCancelled).toBe(true) expect(state.tokenLimitDetected).toBe(true) }) + + test("given ultrawork loop continuation user message update, cancels stale todo continuation countdown", () => { + // given + const sessionID = "ses_ulw_todo_overlap" + const state = sessionStateStore.getState(sessionID) + state.countdownStartedAt = Date.now() - 10_000 + state.wasCancelled = true + state.tokenLimitDetected = true + + // when + handleNonIdleEvent({ + eventType: "message.updated", + properties: { + sessionID, + info: { role: "user" }, + parts: [ + { + type: "text", + text: `ultrawork [SYSTEM DIRECTIVE: OH-MY-OPENCODE - RALPH LOOP 2/500]\ncontinue\n${OMO_INTERNAL_INITIATOR_MARKER}`, + synthetic: true, + }, + ], + }, + sessionStateStore, + }) + + // then + expect(state.countdownStartedAt).toBeUndefined() + expect(state.wasCancelled).toBe(true) + expect(state.tokenLimitDetected).toBe(true) + }) }) diff --git a/src/hooks/todo-continuation-enforcer/non-idle-events.ts b/src/hooks/todo-continuation-enforcer/non-idle-events.ts index d54ae6273..acf685592 100644 --- a/src/hooks/todo-continuation-enforcer/non-idle-events.ts +++ b/src/hooks/todo-continuation-enforcer/non-idle-events.ts @@ -2,6 +2,7 @@ import { resolveMessageEventSessionID, resolveSessionEventID } from "../../share import type { InternalInitiatorTextPartLike } from "../../shared/internal-initiator-marker" import { isSyntheticOrInternalOnlyTextParts } from "../../shared/internal-initiator-marker" import { log } from "../../shared/logger" +import { isSystemDirective } from "../../shared/system-directive" import { COUNTDOWN_GRACE_PERIOD_MS, HOOK_NAME } from "./constants" import type { SessionStateStore } from "./session-state" @@ -34,6 +35,14 @@ function resolveEventParts( return parts } +function hasInternalSystemDirective(parts: InternalInitiatorTextPartLike[] | undefined): boolean { + return (parts ?? []).some( + (part) => part.type === "text" + && typeof part.text === "string" + && isSystemDirective(part.text), + ) +} + export function handleNonIdleEvent(args: { eventType: string properties: Record | undefined @@ -50,6 +59,11 @@ export function handleNonIdleEvent(args: { if (role === "user") { const parts = resolveEventParts(properties) if (isSyntheticOrInternalOnlyTextParts(parts)) { + const state = sessionStateStore.getExistingState(sessionID) + if (state?.countdownStartedAt && hasInternalSystemDirective(parts)) { + sessionStateStore.cancelCountdown(sessionID) + log(`[${HOOK_NAME}] Cancelled countdown for internal continuation message`, { sessionID }) + } log(`[${HOOK_NAME}] Ignoring synthetic/internal user message event`, { sessionID }) return }