Skip to content

Commit ca990f2

Browse files
authored
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>
1 parent 08a73db commit ca990f2

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9365,6 +9365,65 @@ describe("runCodexAppServerAttempt", () => {
93659365
expect(result.timedOut).toBe(false);
93669366
});
93679367

9368+
it("keeps the attempt watchdog armed when terminal projection wedges", async () => {
9369+
const harness = createStartedThreadHarness();
9370+
vi.spyOn(CodexAppServerEventProjector.prototype, "handleNotification").mockImplementation(
9371+
async () => new Promise(() => undefined),
9372+
);
9373+
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
9374+
const params = createParams(
9375+
path.join(tempDir, "session.jsonl"),
9376+
path.join(tempDir, "workspace"),
9377+
);
9378+
params.timeoutMs = 120;
9379+
9380+
const run = runCodexAppServerAttempt(params, {
9381+
turnCompletionIdleTimeoutMs: 5,
9382+
turnTerminalIdleTimeoutMs: 5,
9383+
});
9384+
await harness.waitForMethod("turn/start");
9385+
9386+
void harness.notify({
9387+
method: "turn/completed",
9388+
params: {
9389+
threadId: "thread-1",
9390+
turnId: "turn-1",
9391+
turn: { id: "turn-1", status: "completed" },
9392+
},
9393+
});
9394+
9395+
const result = await Promise.race([
9396+
run,
9397+
new Promise<never>((_, reject) => {
9398+
setTimeout(
9399+
() => reject(new Error("attempt watchdog did not release queued terminal turn")),
9400+
1_000,
9401+
);
9402+
}),
9403+
]);
9404+
expect(result.aborted).toBe(true);
9405+
expect(result.timedOut).toBe(true);
9406+
expect(result.promptError).toBe(
9407+
"codex app-server turn idle timed out waiting for turn/completed",
9408+
);
9409+
expect(
9410+
warn.mock.calls.some(
9411+
([message]) => message === "codex app-server turn idle timed out waiting for progress",
9412+
),
9413+
).toBe(true);
9414+
expect(
9415+
warn.mock.calls.some(
9416+
([message]) =>
9417+
message === "codex app-server turn idle timed out waiting for terminal event",
9418+
),
9419+
).toBe(false);
9420+
expect(
9421+
warn.mock.calls.some(
9422+
([message]) => message === "codex app-server turn idle timed out waiting for completion",
9423+
),
9424+
).toBe(false);
9425+
});
9426+
93689427
it("routes Computer Use MCP elicitations through the native bridge", async () => {
93699428
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
93709429
let handleRequest:

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,6 +1980,8 @@ export async function runCodexAppServerAttempt(
19801980
};
19811981

19821982
const fireTurnAttemptIdleTimeout = () => {
1983+
// terminalTurnNotificationQueued only suppresses short idle guards; a
1984+
// wedged notification queue still needs the full attempt timeout backstop.
19831985
if (completed || runAbortController.signal.aborted || !turnAttemptIdleWatchArmed) {
19841986
return;
19851987
}

0 commit comments

Comments
 (0)