Skip to content

Commit 4076ba0

Browse files
alkor2000openclaw-clownfish[bot]vincentkoc
authored
fix(codex): derive terminal-idle watchdog from explicit run timeout (#85296)
* fix(codex): derive terminal-idle watchdog from effective run timeout Fixes the early-abort half of #85242. The Codex app-server terminal-idle watchdog used a hardcoded 30-minute default that was not derived from the effective run timeout, so a scheduled turn configured with a longer timeoutSeconds could be aborted early at 30 minutes even with budget left. resolveCodexTurnTerminalIdleTimeoutMs (now in attempt-timeouts.ts after the upstream split) accepts the effective run timeout and, with no explicit override, follows the run budget instead of the 30-minute default: - explicit override always wins (advanced config / tests) - otherwise terminal-idle = max(30min floor, run budget), so a longer run is no longer cut short and existing protection is never shortened - falls back to the 30min default when no run budget is known Reuses the existing resolvePositiveIntegerTimeoutMs helper, matching the neighbouring post-tool resolver. Adds focused unit tests for the derivation. The diagnostic-wording half of #85242 (naming the terminal-idle watchdog in the surfaced guidance) is left as a separate follow-up. * fix(codex): derive terminal-idle watchdog from effective run timeout * fix(codex): preserve default terminal idle watchdog --------- Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com> Co-authored-by: Vincent Koc <[email protected]>
1 parent 4391041 commit 4076ba0

4 files changed

Lines changed: 69 additions & 2 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@ describe("Codex app-server attempt timeouts", () => {
112112
);
113113
});
114114

115+
it("derives the terminal idle timeout from the effective run budget", () => {
116+
const overFloor = CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS + 15 * 60_000;
117+
// A run budget above the 30-minute floor extends the watchdog (the #85242 fix).
118+
expect(resolveCodexTurnTerminalIdleTimeoutMs(undefined, overFloor)).toBe(overFloor);
119+
// A run budget below the floor keeps the 30-minute floor (protection never shortened).
120+
expect(resolveCodexTurnTerminalIdleTimeoutMs(undefined, 10 * 60_000)).toBe(
121+
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
122+
);
123+
// A non-finite budget falls back to the 30-minute default.
124+
expect(resolveCodexTurnTerminalIdleTimeoutMs(undefined, Number.POSITIVE_INFINITY)).toBe(
125+
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
126+
);
127+
expect(resolveCodexTurnTerminalIdleTimeoutMs(undefined, Number.MAX_SAFE_INTEGER)).toBe(
128+
MAX_TIMER_TIMEOUT_MS,
129+
);
130+
// An explicit override still wins even when a run budget is present.
131+
expect(resolveCodexTurnTerminalIdleTimeoutMs(5 * 60_000, overFloor)).toBe(5 * 60_000);
132+
});
133+
115134
it("caps gateway timeout grace", () => {
116135
expect(resolveCodexGatewayTimeoutWithGraceMs(120_000)).toBe(130_000);
117136
expect(resolveCodexGatewayTimeoutWithGraceMs(120_000, 500)).toBe(120_500);

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,19 @@ export function resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(
117117
}
118118

119119
/** Resolves the long terminal turn idle timeout. */
120-
export function resolveCodexTurnTerminalIdleTimeoutMs(value: number | undefined): number {
121-
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS);
120+
export function resolveCodexTurnTerminalIdleTimeoutMs(
121+
value: number | undefined,
122+
runTimeoutOverrideMs?: number,
123+
): number {
124+
// The terminal watchdog is wrapper-owned; Codex turn options do not carry a
125+
// timeout budget. Follow explicit per-run intent without replacing the floor
126+
// with the implicit 48-hour agent default.
127+
const explicitRunBudgetMs = resolvePositiveIntegerTimeoutMs(
128+
runTimeoutOverrideMs,
129+
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
130+
);
131+
const defaultMs = Math.max(CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS, explicitRunBudgetMs);
132+
return resolvePositiveIntegerTimeoutMs(value, defaultMs);
122133
}
123134

124135
/** Adds gateway grace time to a caller timeout without overflowing invalid values. */

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,6 +1621,7 @@ export async function runCodexAppServerAttempt(
16211621
);
16221622
const turnTerminalIdleTimeoutMs = resolveCodexTurnTerminalIdleTimeoutMs(
16231623
options.turnTerminalIdleTimeoutMs,
1624+
params.runTimeoutOverrideMs,
16241625
);
16251626
const turnAttemptIdleTimeoutMs = Math.max(100, Math.floor(params.timeoutMs));
16261627
let nativeHookRelayLastRenewedAt = 0;

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,42 @@ describe("createCodexAttemptTurnWatchController", () => {
122122
});
123123

124124
describe("runCodexAppServerAttempt turn watches", () => {
125+
it.each([
126+
{
127+
name: "keeps the 30-minute floor for the implicit 48-hour run timeout",
128+
runTimeoutOverrideMs: undefined,
129+
expectedTerminalIdleTimeoutMs: 30 * 60_000,
130+
},
131+
{
132+
name: "follows an explicit 45-minute run timeout",
133+
runTimeoutOverrideMs: 45 * 60_000,
134+
expectedTerminalIdleTimeoutMs: 45 * 60_000,
135+
},
136+
])("$name", async ({ runTimeoutOverrideMs, expectedTerminalIdleTimeoutMs }) => {
137+
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
138+
const harness = createStartedThreadHarness();
139+
const params = createParams(
140+
path.join(tempDir, "session.jsonl"),
141+
path.join(tempDir, "workspace"),
142+
);
143+
params.timeoutMs = 48 * 60 * 60_000;
144+
params.runTimeoutOverrideMs = runTimeoutOverrideMs;
145+
const run = runCodexAppServerAttempt(params);
146+
147+
await harness.waitForMethod("turn/start");
148+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expectedTerminalIdleTimeoutMs);
149+
await harness.notify({
150+
method: "turn/completed",
151+
params: {
152+
threadId: "thread-1",
153+
turnId: "turn-1",
154+
turn: { id: "turn-1", status: "completed", items: [] },
155+
},
156+
});
157+
158+
await expect(run).resolves.toMatchObject({ aborted: false, timedOut: false });
159+
});
160+
125161
it("releases the session when Codex never completes after a dynamic tool response", async () => {
126162
let handleRequest:
127163
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)

0 commit comments

Comments
 (0)