diff --git a/extensions/codex/src/app-server/attempt-turn-watches.test.ts b/extensions/codex/src/app-server/attempt-turn-watches.test.ts index b7aa6f2f945..4c46cdff549 100644 --- a/extensions/codex/src/app-server/attempt-turn-watches.test.ts +++ b/extensions/codex/src/app-server/attempt-turn-watches.test.ts @@ -171,35 +171,29 @@ describe("Codex app-server attempt turn watches", () => { expect(harness.abortController.signal.aborted).toBe(false); }); - it("fires terminal idle timeout while an app-server request is in flight", () => { + it("keeps terminal idle gated while an app-server request is in flight", () => { const harness = createController(); harness.activeRequests = 1; harness.controller.armTerminalIdleWatch(); vi.advanceTimersByTime(10); - expect(harness.timeouts).toMatchObject([ - { - kind: "terminal", - idleMs: 10, - timeoutMs: 10, - lastActivityReason: "startup", - }, - ]); - expect(harness.abortController.signal.reason).toBe("turn_terminal_idle_timeout"); + expect(harness.timeouts).toEqual([]); + expect(harness.abortController.signal.aborted).toBe(false); }); - it("keeps terminal idle alive when notifications arrive during an in-flight request", () => { + it("fires terminal idle after the in-flight request settles and silence resumes", () => { const harness = createController(); harness.activeRequests = 1; harness.controller.armTerminalIdleWatch(); - vi.advanceTimersByTime(9); - harness.controller.noteNotificationReceived("item/commandExecution/outputDelta"); - vi.advanceTimersByTime(9); - + vi.advanceTimersByTime(10); + expect(harness.timeouts).toEqual([]); + + harness.activeRequests = 0; + harness.controller.touchActivity("request:item/tool/call:response"); + vi.advanceTimersByTime(9); expect(harness.timeouts).toEqual([]); - expect(harness.abortController.signal.aborted).toBe(false); vi.advanceTimersByTime(1); expect(harness.timeouts).toMatchObject([ @@ -207,9 +201,10 @@ describe("Codex app-server attempt turn watches", () => { kind: "terminal", idleMs: 10, timeoutMs: 10, - lastActivityReason: "notification:item/commandExecution/outputDelta", + lastActivityReason: "request:item/tool/call:response", }, ]); + expect(harness.abortController.signal.reason).toBe("turn_terminal_idle_timeout"); }); it("keeps completion idle gated while a request is in flight", () => { diff --git a/extensions/codex/src/app-server/attempt-turn-watches.ts b/extensions/codex/src/app-server/attempt-turn-watches.ts index 7bfbc0eb542..1f996cd00cf 100644 --- a/extensions/codex/src/app-server/attempt-turn-watches.ts +++ b/extensions/codex/src/app-server/attempt-turn-watches.ts @@ -166,7 +166,12 @@ export function createCodexAttemptTurnWatchController(params: { function scheduleTerminalIdleWatch() { clearTerminalIdleTimer(); - if (params.isCompleted() || params.signal.aborted || !terminalIdleWatchArmed) { + if ( + params.isCompleted() || + params.signal.aborted || + !terminalIdleWatchArmed || + params.getActiveAppServerTurnRequests() > 0 + ) { return; } const elapsedMs = Math.max(0, Date.now() - completionLastActivityAt); @@ -366,14 +371,18 @@ export function createCodexAttemptTurnWatchController(params: { } function fireTerminalIdleTimeout() { - // This is the physical-client liveness backstop. An in-flight request can - // be silent forever if the client is wedged, so only real notifications - // reset the terminal clock. + // Physical-client liveness backstop. A terminal timeout retires the shared + // client, so it must only measure silence the client owns: while a + // server->client request is pending (approval/elicitation/tool call) the + // app-server legitimately says nothing until we respond. The response path + // touches activity when the request settles, so a wedged client is still + // caught within one terminal window after our response. if ( params.isCompleted() || params.isTerminalTurnNotificationQueued() || params.signal.aborted || - !terminalIdleWatchArmed + !terminalIdleWatchArmed || + params.getActiveAppServerTurnRequests() > 0 ) { return; }