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 <vincentkoc@ieee.org>
This commit is contained in:
Vincent Koc
2026-05-27 08:50:59 +01:00
committed by GitHub
parent 08a73dbe4b
commit ca990f2ce1
2 changed files with 61 additions and 0 deletions

View File

@@ -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<never>((_, 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<void> = async () => undefined;
let handleRequest:

View File

@@ -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;
}