From ca990f2ce1cafbc36f7df097ee90e00d208a3bb7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 27 May 2026 08:50:59 +0100 Subject: [PATCH] fix(codex): keep attempt watchdog for queued terminal turns Keep the Codex app-server full attempt watchdog armed after a terminal turn notification is queued, so a wedged notification projector cannot leave a run stuck indefinitely. Proof: - `git diff --check origin/main...HEAD` - `node scripts/run-oxlint.mjs extensions/codex/src/app-server/run-attempt.ts extensions/codex/src/app-server/run-attempt.test.ts` - `node scripts/run-vitest.mjs run extensions/codex/src/app-server/run-attempt.test.ts --testNamePattern "keeps the attempt watchdog armed"` passed in PR proof (`1 passed | 232 skipped`) - `OPENCLAW_TESTBOX=1 pnpm check:changed` passed in `tbx_01kskyg44ej461k574jee8ffjc` - CI required checks green after `build-artifacts` rerun job `78031279635` passed Co-authored-by: Vincent Koc --- .../codex/src/app-server/run-attempt.test.ts | 59 +++++++++++++++++++ .../codex/src/app-server/run-attempt.ts | 2 + 2 files changed, 61 insertions(+) diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 949f4fd1c47..a855cd00c69 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -9365,6 +9365,65 @@ describe("runCodexAppServerAttempt", () => { expect(result.timedOut).toBe(false); }); + it("keeps the attempt watchdog armed when terminal projection wedges", async () => { + const harness = createStartedThreadHarness(); + vi.spyOn(CodexAppServerEventProjector.prototype, "handleNotification").mockImplementation( + async () => new Promise(() => undefined), + ); + const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); + const params = createParams( + path.join(tempDir, "session.jsonl"), + path.join(tempDir, "workspace"), + ); + params.timeoutMs = 120; + + const run = runCodexAppServerAttempt(params, { + turnCompletionIdleTimeoutMs: 5, + turnTerminalIdleTimeoutMs: 5, + }); + await harness.waitForMethod("turn/start"); + + void harness.notify({ + method: "turn/completed", + params: { + threadId: "thread-1", + turnId: "turn-1", + turn: { id: "turn-1", status: "completed" }, + }, + }); + + const result = await Promise.race([ + run, + new Promise((_, reject) => { + setTimeout( + () => reject(new Error("attempt watchdog did not release queued terminal turn")), + 1_000, + ); + }), + ]); + expect(result.aborted).toBe(true); + expect(result.timedOut).toBe(true); + expect(result.promptError).toBe( + "codex app-server turn idle timed out waiting for turn/completed", + ); + expect( + warn.mock.calls.some( + ([message]) => message === "codex app-server turn idle timed out waiting for progress", + ), + ).toBe(true); + expect( + warn.mock.calls.some( + ([message]) => + message === "codex app-server turn idle timed out waiting for terminal event", + ), + ).toBe(false); + expect( + warn.mock.calls.some( + ([message]) => message === "codex app-server turn idle timed out waiting for completion", + ), + ).toBe(false); + }); + it("routes Computer Use MCP elicitations through the native bridge", async () => { let notify: (notification: CodexServerNotification) => Promise = async () => undefined; let handleRequest: diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 05a914fd7f2..5e503de314e 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -1980,6 +1980,8 @@ export async function runCodexAppServerAttempt( }; const fireTurnAttemptIdleTimeout = () => { + // terminalTurnNotificationQueued only suppresses short idle guards; a + // wedged notification queue still needs the full attempt timeout backstop. if (completed || runAbortController.signal.aborted || !turnAttemptIdleWatchArmed) { return; }