Skip to content

fix(session): release the retry gate when auto_retry_start delivery fails - #4169

Open
probepark wants to merge 1 commit into
Yeachan-Heo:devfrom
probepark:fix/issue-4056-dead-turn
Open

fix(session): release the retry gate when auto_retry_start delivery fails#4169
probepark wants to merge 1 commit into
Yeachan-Heo:devfrom
probepark:fix/issue-4056-dead-turn

Conversation

@probepark

Copy link
Copy Markdown
Collaborator

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-377 converts a run-loop exception to stream.fail(err)
  • packages/agent/src/agent.ts:1862-1891 catches it, creates an error assistant message, calls requestRunTerminal
  • agent.ts:1304-1327 and :1983-2028 publish exactly one agent_end and seal the run
  • its finally at agent.ts:1926-1937 clears isStreaming, active run ids, the abort controller, and resolves the running prompt
  • agent-session.ts:9195-9206 (#settleEndedInFlight) releases prompt admission; counter at :2622-2640; busy predicate at :6487-6490

The agent really does seal correctly. The gap is that the session never learns.

Root cause

#handleRetryableError creates #retryPromise, then awaits delivery of the auto_retry_start extension event before scheduling the retry.

If that delivery rejects, the event handler swallows the error. The retry is never scheduled, so #retryPromise is never resolved and #promptInFlightCount stays 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:

  • Swallow the delivery error and schedule the retry anyway — runs a retry the extension never acknowledged.
  • Bound the await with a timeout — holds the gate for the full timeout on every failure, converting a hang into a stall.

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

run result
bun test packages/coding-agent/test/agent-session-retry-busy-recovery.test.ts 7 pass / 0 fail
mutation — remove the catch, keep the test 0 pass / 1 fail, by timeout
re-apply 7 pass / 0 fail
bun --cwd=packages/coding-agent run check:types exit 0
biome 2.5.2 on both changed files clean

The regression asserts the observable contract that matters: the same AgentSession accepts 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.isStreaming and active run id, AgentSession prompt-in-flight and retry state, and SDK turn.prompt_status for the same turn.

@probepark

Copy link
Copy Markdown
Collaborator Author

CI flake, not this PR. Rerun queued.

Affected path validation / test:.../notifications-live-stream.test.ts failed with:

error: notifications: SDK startup failed: Telegram daemon did not become ready after spawning
(fail) a mid-turn disable retains the streamed message for its authoritative final, then cleanly restarts next turn

Two reasons this is not attributable here:

  1. This PR touches no notification code. The diff is exactly two files — packages/coding-agent/src/session/agent-session.ts and packages/coding-agent/test/agent-session-retry-busy-recovery.test.ts.
  2. The suite passes on clean dev. I ran it on a pristine checkout of d1d4807a2: 20 pass / 0 fail.

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.

…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
probepark force-pushed the fix/issue-4056-dead-turn branch from 798c6e9 to 182232c Compare August 11, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant