Skip to content

fix: re-arm Responses request-body timeout recovery on turn output - #12654

Open
hellofrommorgan wants to merge 3 commits into
can1357:mainfrom
hellofrommorgan:fix/reset-request-body-timeout-guard
Open

hellofrommorgan wants to merge 3 commits into
can1357:mainfrom
hellofrommorgan:fix/reset-request-body-timeout-guard

Conversation

@hellofrommorgan

@hellofrommorgan hellofrommorgan commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #11878. The one-shot full-replay request-body-timeout recovery is now re-armed by any turn that produced output, instead of staying spent for the entire user prompt.

In the contributor's own words: "should we submit a follow-up PR for…infinite 408s""implement … the reset guard on successful turn".

TurnRecovery.#requestBodyReadTimeoutRecoveryPromptSequence was set to the logical prompt sequence before the shake and never cleared. onAssistantSettledSuccessfully now clears it, so the guard bounds unchanged-request loops rather than the lifetime of a prompt.

Why

#11878 correctly stopped the 5×61s identical-replay loop, and #11878's review moved the marker from promptGeneration() to promptSequence() so a later user prompt could still recover. But one user prompt in an orchestrator/agentic run is hundreds of assistant turns, and history keeps growing after a successful recovery.

Observed in a real session (gpt-6-astra via github-copilot, omp 18.2.6):

  • 20:42:37Z — exact 408 user_request_timeout / "Timed out reading request body", requestBodyReadTimeoutFullReplay: true, one 62.5s attempt. Recovery fired, shook tool-result bulk into an artifact, retried once, succeeded (retryRecovery.status: "recovered", superseded by a real assistant response).
  • ~170 further tool calls in the same user prompt.
  • 21:12:36Z — second exact 408, again one 62.2s attempt, requestBodyReadTimeoutFullReplay: true, no retryRecovery field: the guard short-circuited to handled-terminal while 72 un-elided tool results and full retry budget (#retryAttempt was back to 0) were still available. The session stopped until the operator ran /compact and re-prompted.

Only the first condition in that if can explain it: retrySettings.enabled was true, maxRetries (10) <= #retryAttempt (0) was false (the success path resets #retryAttempt), the session was neither disposed, aborting, nor compacting, and the failing turn had empty content so it was not replay-unsafe.

Back-to-back timeouts (no intervening output) never reach onAssistantSettledSuccessfully, so the original loop bound is unchanged.

Testing

  • Reproduction as a regression testpackages/coding-agent/test/agent-session-responses-body-read-timeout.test.ts: "recovers a second full-replay timeout in the same prompt after an intervening successful turn". Drives a real AgentSession against a local Responses server through 408 → shake+retry → tool turn → assistant turn → 408 → shake+retry → completion inside one prompt. Red before the fix (4 requests, second 408 terminal), green after (5 requests, second recovery elides only the new tool-result bulk into artifact://, assistant text preserved).
  • Guard lifecycle unitspackages/coding-agent/test/turn-recovery-replay-unsafe.test.ts, new full-replay timeout one-shot lifecycle block: re-arms after a turn that produced output (red before the fix); stays handled-terminal on back-to-back timeouts; does not re-arm on a settled turn that produced no output. The two bounding cases pass both before and after, so the loop protection is pinned, not loosened.
  • bun test on the two touched files plus shake.test.ts: 123 pass, 0 fail.
  • Retry/recovery-adjacent suites (agent-session-retry-recovery, -retry-cap, -retry-fallback, -empty-stop-guard, -terminal-error-persistence, -thinking-loop-retry, -yield-empty-stop-suppression, -auto-compaction-queue, event-controller-error-banner, repro-issue-6879-tool-double-render-retry, issue-2750-subagent-runtime-fallback): 269 pass, 0 fail.
  • bun run ci:check:full: exit 0 (types + oxlint + oxfmt clean).
  • bun check: check:ts passes; check:rs fails on rustfmt diffs in crates/pi-natives that reproduce on a pristine checkout of main at 936f9762d2 with no local changes, so it is pre-existing and unrelated.
  • No live Copilot 408 was reproduced against the gateway; the failing sequence was exercised locally and against the recorded session transcript above.

Disclosure

This pull request transparently discloses AI-assisted implementation and testing, submitted with the author's authorization. No separate human-review, human-exercise, or human-authored explanation claim is made beyond the contributor sentence quoted above, which is the contributor's own wording.


  • bun check passes — not claimed: check:ts passes, but check:rs exits 1 on rustfmt drift in crates/pi-natives that reproduces identically on a pristine main at 936f9762d2 with no local changes. bun run ci:check:full (types + oxlint + oxfmt) is exit 0. See Testing.
  • Tested locally — automated suites only; see Testing for the exact scenarios and the explicit no-live-gateway-reproduction note.
  • CHANGELOG updated with the required attribution (if user-facing; internal issue fixes use issue links, external contributions add the PR link and contributor credit after creation)

…n turn output

The one-shot full-replay request-body-timeout recovery was keyed to the
logical prompt sequence and never cleared, so a single long-running prompt
got exactly one shake-and-retry for its entire lifetime. A second exact 408
later in the same prompt - over history that grew after the first recovery -
terminated the turn even with eligible tool-result bulk and retry budget left.

Clearing the marker in onAssistantSettledSuccessfully re-arms the recovery on
any turn that produced output. Back-to-back timeouts never reach that path, so
an unchanged-request loop is still bounded to one changed request.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@roboomp roboomp added agent Agent runtime planning and orchestration fix review:p0 triaged labels Sep 22, 2026

@roboomp roboomp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 — clean root-cause fix. onAssistantSettledSuccessfully (turn-recovery.ts:435) clears #requestBodyReadTimeoutRecoveryPromptSequence only after the assistantTurnProducedOutput filter, so the one-shot full-replay recovery re-arms on genuine forward progress while the guard at line 1267 still short-circuits back-to-back timeouts to handled-terminal — the original loop bound is preserved. Callsite (agent-session.ts:3328) fires on every settled assistant message; the internal filter correctly excludes error/aborted/empty turns.

Tests are strong: the integration case drives a real AgentSession through 408 -> shake+retry -> tool turn -> assistant turn -> 408 -> recovery -> completion and asserts observable request bytes + final message; the three lifecycle units pin re-arm, no-progress-bound, and empty-output-no-rearm. CHANGELOG (Unreleased/Fixed) and docs both updated.

Only a test-only nit inline (AgentTool<any, any, any> vs the no-any convention). Nothing blocking. Thanks for the thorough repro write-up and disclosure, @hellofrommorgan.

Comment thread packages/coding-agent/test/agent-session-responses-body-read-timeout.test.ts Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Agent runtime planning and orchestration fix review:p0 triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants