fix(session): release the retry gate when auto_retry_start delivery fails - #4169
Open
probepark wants to merge 1 commit into
Open
fix(session): release the retry gate when auto_retry_start delivery fails#4169probepark wants to merge 1 commit into
probepark wants to merge 1 commit into
Conversation
probepark
force-pushed
the
fix/issue-4056-dead-turn
branch
2 times, most recently
from
August 10, 2026 09:39
211305d to
168ef75
Compare
Collaborator
Author
|
CI flake, not this PR. Rerun queued.
Two reasons this is not attributable here:
The failure text is a spawn-readiness timeout, which is the same class of problem as #4146 — under CI load a child does not reach readiness inside its budget and the failure surfaces as something unrelated-looking. Worth noting that this suite is a repeat offender under load; if it keeps flaking it deserves its own issue about the daemon readiness budget rather than being re-run each time. |
probepark
force-pushed
the
fix/issue-4056-dead-turn
branch
from
August 10, 2026 11:02
168ef75 to
edaeeb4
Compare
This was referenced Aug 10, 2026
Closed
probepark
force-pushed
the
fix/issue-4056-dead-turn
branch
4 times, most recently
from
August 10, 2026 13:24
18e37e0 to
798c6e9
Compare
…ails #handleRetryableError created #retryPromise, then awaited delivery of the auto_retry_start extension event before scheduling the retry. When that delivery rejected, the event handler swallowed the error and the retry was never scheduled -- so #retryPromise stayed unresolved and #promptInFlightCount stayed positive forever. The agent had already sealed its run correctly; the session just never learned. That is the dead turn in a live session: the user cannot submit again because admission was never released. Terminalize retry recovery on delivery failure, clear the retry controller, and release the gate. Lore-id: 4056b7d2 Constraint: same-session continuation only -- no new session, no resume state machine Rejected: swallow the delivery error and schedule the retry anyway | runs a retry the extension never acknowledged Rejected: bound the wait with a timeout | leaves the gate held for the timeout on every failure Confidence: high Scope-risk: narrow Reversibility: easy Tested: removing the catch turns 7 pass / 0 fail into 0 pass / 1 fail by timeout Not-tested: whether the provider-interrupt report also has a second cause outside the retry path
probepark
force-pushed
the
fix/issue-4056-dead-turn
branch
from
August 11, 2026 01:31
798c6e9 to
182232c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4056 — "Provider stream interruption can leave a dead turn in a live session".
Why source reading said this was impossible
The provider-abort path looks correct end to end, and I verified all of it:
packages/agent/src/agent-loop.ts:373-377converts a run-loop exception tostream.fail(err)packages/agent/src/agent.ts:1862-1891catches it, creates an error assistant message, callsrequestRunTerminalagent.ts:1304-1327and:1983-2028publish exactly oneagent_endand seal the runfinallyatagent.ts:1926-1937clearsisStreaming, active run ids, the abort controller, and resolves the running promptagent-session.ts:9195-9206(#settleEndedInFlight) releases prompt admission; counter at:2622-2640; busy predicate at:6487-6490The agent really does seal correctly. The gap is that the session never learns.
Root cause
#handleRetryableErrorcreates#retryPromise, then awaits delivery of theauto_retry_startextension event before scheduling the retry.If that delivery rejects, the event handler swallows the error. The retry is never scheduled, so
#retryPromiseis never resolved and#promptInFlightCountstays positive forever. The busy predicate is only agent streaming plus that counter — the agent is idle, but the counter is stuck, so the session refuses every subsequent prompt.That is the dead turn: a live session that will not accept input, with no error surfaced, because admission was never released.
It also explains why the report is intermittent and correlates with unstable connections — you need a stream failure and a failing extension delivery in the same window.
Fix
On delivery failure: terminalize retry recovery, clear the retry controller, release the gate.
Rejected alternatives:
Scope
Same-session continuation only. No new session, no duplicate worktree writer, no resume state machine — #4056 asks for those as a feature and they remain out of scope.
Verification
bun test packages/coding-agent/test/agent-session-retry-busy-recovery.test.tsbun --cwd=packages/coding-agent run check:typesThe regression asserts the observable contract that matters: the same
AgentSessionaccepts a subsequent prompt after the failure.Mutation and typecheck re-run by me against the committed branch.
Pre-existing failures, not from this PR
--test-name-pattern "retry"across the repo gives 125 pass / 241 fail with this change. On a stashed tree at the same head it is 124 pass / 241 fail — identical failure count, one extra pass (the new test). Those 241 are pre-existing and unrelated; flagging them rather than absorbing them.Not covered
Whether the reporter's provider-interrupt scenario also has a second cause outside the retry path. This closes the one window I could prove; if it recurs, the fields worth capturing are the provider stream error and timing,
Agent.isStreamingand active run id, AgentSession prompt-in-flight and retry state, and SDKturn.prompt_statusfor the same turn.