Problem
--turn-timeout-ms / CODEX_TURN_TIMEOUT_MS currently bound the entire turn, not agent inactivity. In captureTurn (plugins/codex/scripts/lib/codex.mjs ~617-650), a single setTimeout is armed before startRequest() and races the whole turn lifecycle — it never resets while the turn is actively producing item/started/item/completed notifications.
This is the wrong metric. gpt-5.6-sol at effort=xhigh on adversarial-review routinely does real, visibly-progressing work for well over 10 minutes — it is not stalled, it's just slow at high effort on a large diff. A fixed wall-clock budget forces an impossible choice:
Design: idle-timeout instead of wall-clock budget
Replace the fixed turn budget with an inactivity (idle) timeout: the deadline resets on every turn-lifecycle event (turn/started, item/started, item/completed), so a turn that's continuously producing output is never killed no matter how long it runs in total — only a period of silence (no events) longer than the budget triggers the timeout.
captureTurn (~617): replace the one-shot setTimeout-backed deadline promise with a resettable deadline stored on the turn-capture state (arm/reset/clear helpers), instead of a single race set up before startRequest().
applyTurnNotification (~548) is the single choke point all turn notifications already flow through — reset the idle deadline there on turn/started, item/started, item/completed.
- Keep a coarse wall-clock ceiling as a second, generous failsafe (~30-60 min) — a backstop against a turn that resets the idle timer forever without ever actually finishing (e.g. a runaway tool-call loop), distinct from the idle budget.
- Update
--turn-timeout-ms help text / comments: the meaning changes from "budget for the whole turn" to "how long the turn may go silent between events before it's considered dead."
Why this doesn't regress the openai#376 fix
openai#376's actual fix was making the completion await reject-able at all (dead code rejectCompletion was never wired) plus resolving the budget at call time instead of import time. Both of those are orthogonal to what the timer measures and are preserved as-is — this issue only changes the deadline from "single shot at turn start" to "resettable, keyed off actual turn activity."
Interaction with upstream openai#407 (structured-retry loop)
Upstream openai#407 (open PR, not yet in this fork) adds an opt-in structuredRetryPrompt retry loop to runAppServerTurn: if the captured message isn't valid JSON, it resumes the same thread with a JSON-only prompt, up to structuredRetryAttempts (default 3) additional captureTurn calls on top of the original.
Under the current wall-clock design, if that retry loop is ever ported here, each retry attempt gets its own fresh DEFAULT_TURN_TIMEOUT_MS/--turn-timeout-ms budget — so a worst case of 1 initial + 3 retries could consume up to 4x the configured turn budget before finally failing, silently multiplying the caller's stated timeout.
Under an idle-timeout design this doesn't happen: each captureTurn invocation (initial or retry) is bounded by its own idle-silence window, not by an aggregate clock, so there's nothing to multiply — the retry loop's total wall-clock time is bounded by (attempts × real work time), which is exactly what the caller wants, not (attempts × worst-case-budget).
Scope
This is a separate concern from #38 (turnTimeoutMs not forwarded to the adversarial-review call site at all — fixed in #39). This issue is about what the timeout measures, once it is forwarded everywhere.
Problem
--turn-timeout-ms/CODEX_TURN_TIMEOUT_MScurrently bound the entire turn, not agent inactivity. IncaptureTurn(plugins/codex/scripts/lib/codex.mjs~617-650), a singlesetTimeoutis armed beforestartRequest()and races the whole turn lifecycle — it never resets while the turn is actively producingitem/started/item/completednotifications.This is the wrong metric.
gpt-5.6-solateffort=xhighonadversarial-reviewroutinely does real, visibly-progressing work for well over 10 minutes — it is not stalled, it's just slow at high effort on a large diff. A fixed wall-clock budget forces an impossible choice:Design: idle-timeout instead of wall-clock budget
Replace the fixed turn budget with an inactivity (idle) timeout: the deadline resets on every turn-lifecycle event (
turn/started,item/started,item/completed), so a turn that's continuously producing output is never killed no matter how long it runs in total — only a period of silence (no events) longer than the budget triggers the timeout.captureTurn(~617): replace the one-shotsetTimeout-backeddeadlinepromise with a resettable deadline stored on the turn-capture state (arm/reset/clear helpers), instead of a single race set up beforestartRequest().applyTurnNotification(~548) is the single choke point all turn notifications already flow through — reset the idle deadline there onturn/started,item/started,item/completed.--turn-timeout-mshelp text / comments: the meaning changes from "budget for the whole turn" to "how long the turn may go silent between events before it's considered dead."Why this doesn't regress the openai#376 fix
openai#376's actual fix was making the completion
awaitreject-able at all (dead coderejectCompletionwas never wired) plus resolving the budget at call time instead of import time. Both of those are orthogonal to what the timer measures and are preserved as-is — this issue only changes the deadline from "single shot at turn start" to "resettable, keyed off actual turn activity."Interaction with upstream openai#407 (structured-retry loop)
Upstream openai#407 (open PR, not yet in this fork) adds an opt-in
structuredRetryPromptretry loop torunAppServerTurn: if the captured message isn't valid JSON, it resumes the same thread with a JSON-only prompt, up tostructuredRetryAttempts(default 3) additionalcaptureTurncalls on top of the original.Under the current wall-clock design, if that retry loop is ever ported here, each retry attempt gets its own fresh
DEFAULT_TURN_TIMEOUT_MS/--turn-timeout-msbudget — so a worst case of 1 initial + 3 retries could consume up to 4x the configured turn budget before finally failing, silently multiplying the caller's stated timeout.Under an idle-timeout design this doesn't happen: each
captureTurninvocation (initial or retry) is bounded by its own idle-silence window, not by an aggregate clock, so there's nothing to multiply — the retry loop's total wall-clock time is bounded by (attempts × real work time), which is exactly what the caller wants, not (attempts × worst-case-budget).Scope
This is a separate concern from #38 (
turnTimeoutMsnot forwarded to the adversarial-review call site at all — fixed in #39). This issue is about what the timeout measures, once it is forwarded everywhere.