Skip to content

fix(retry-fallback): exhaustion event leads with the primary failure - #5058

Open
code-yeongyu wants to merge 4 commits into
mainfrom
fix/4084-exhaustion-primary-failure
Open

code-yeongyu wants to merge 4 commits into
mainfrom
fix/4084-exhaustion-primary-failure

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

When a turn exhausts its fallback chain, retry_fallback_exhausted now carries the failure that started the walk and every rung that failed after it, not only the last rung's error. The TUI notice and the print-mode stderr line lead with the primary failure and list the later rungs as then <model>: <error>. lastError is unchanged for older clients.

Root cause

  • The event type was { type: "retry_fallback_exhausted"; chainKey; lastError } (packages/coding-agent/src/core/agent-session.ts:765).
  • Every emission site passed the error of the rung that had just failed (e.g. agent-session.ts:12966, lastError: errorMessage), so nothing recorded the primary's failure. In the reported sequence (primary 400 "out of extra usage", sibling 400, then another provider's Connection error.), every surface showed only Connection error., and triage chased a network problem that did not exist.

Fix

  • retry-fallback/exhaustion.ts: typed FallbackRungFailure { model, reason, error }, recordRungFailure (drops an exact repeat, since one failure can be offered to the walk twice), and describeFallbackExhaustion, the single primary-first text both surfaces use.
  • agent-session.ts: the session records each rung failure when it offers it to the fallback walk (per turn; cleared where the controller's turn state resets). The eight exhaustion emissions go through one _emitFallbackExhausted, which adds primaryError and failures. The client projection in _emit passes each rung's text through the same account-usage-limit description already applied to lastError. The provider text is the stream-boundary-redacted errorMessage, the same text lastError already carried.
  • interactive-mode.ts: when earlier rungs failed, the exhaustion notice leads with the primary failure, even if the last rung's error was retryable (which used to route the card to the generic provider-error summary that showed only that error). The single-rung path is unchanged.
  • print-mode.ts: Model fallback exhausted: <chain> (<primary>; then <rung>: <error>; ...).

RED -> GREEN

  • Test: packages/coding-agent/test/suite/regressions/4084-exhaustion-primary-failure.test.ts replays the reported shape through the faux harness: the primary fails with a 400, the second rung with the same 400, and the last rung with Connection error.. It then asserts the event, the print line and the TUI notice.
  • RED at 1261291012 (test only): 3 failed. The event had no primaryError (expected undefined to deeply equal { model: 'faux/faux-1', ... }), the print line did not contain the 400, and the TUI notice did not lead with the primary.
  • GREEN: CI=1 bun run test (from packages/coding-agent) on the new test plus the existing exhaustion-surface tests test/interactive-mode-fallback-error-sanitization.test.ts, test/interactive-mode-fallback.test.ts, test/print-mode-fallback-notice.test.ts and test/suite/regressions/2894-fallback-budget-refused-rung.test.ts: 5 files, 18 passed, 0 failed.
  • Repo-wide checks (bun run check: lint, format, typecheck) and the whole suite run in the sanctioned static and full receipts at this head, not as hand-run gates.

Compatibility

primaryError and failures are optional fields. Events built by older code, and the RPC/JSON stream for consumers that ignore unknown fields, are unaffected. lastError keeps its meaning.

Part of #4084 (this is the engine and TUI/print half). The desktop card (providerFailurePresentation.ts) lives in the desktop repo and still has to read primaryError.

Part of #4090


Summary by cubic

Fixes retry_fallback_exhausted events so they lead with the failure that started the fallback walk instead of only the last rung's error, so an account-limit 400 no longer hides behind a later connection error.

  • The event now includes optional primaryError and failures; lastError keeps its meaning for older clients.
  • The TUI notice and print-mode stderr line lead with the primary failure and list later rungs as then <model>: <error>.
  • The session records each rung failure per turn (dropping exact repeats) and runs each rung's text through the same account-limit description already applied to lastError.
  • Adds changelog fragments and a regression test that replays the reported failure shape.

Written for commit d51dfab. Summary will update on new commits.

View guided diff Turn on auto-fix

… every failed rung (#4084)

retry_fallback_exhausted carried only lastError, the error of the rung that ran last, so a primary 400 hid behind a later rung's connection error. The session now records each failed rung (model, reason, error) per turn and the event carries primaryError plus failures, with lastError kept for older clients; client projection runs each rung's text through the same account-limit description as lastError. TUI and print mode lead with the primary failure.

This branch has not been deployed

No deployments
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