fix(retry-fallback): exhaustion event leads with the primary failure
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'sConnection error.), every surface showed onlyConnection error., and triage chased a network problem that did not exist.
Fix
retry-fallback/exhaustion.ts: typedFallbackRungFailure { model, reason, error },recordRungFailure(drops an exact repeat, since one failure can be offered to the walk twice), anddescribeFallbackExhaustion, 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 addsprimaryErrorandfailures. The client projection in_emitpasses each rung's text through the same account-usage-limit description already applied tolastError. The provider text is the stream-boundary-redactederrorMessage, the same textlastErroralready 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.tsreplays the reported shape through the faux harness: the primary fails with a 400, the second rung with the same 400, and the last rung withConnection error.. It then asserts the event, the print line and the TUI notice. - RED at
1261291012(test only): 3 failed. The event had noprimaryError(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(frompackages/coding-agent) on the new test plus the existing exhaustion-surface teststest/interactive-mode-fallback-error-sanitization.test.ts,test/interactive-mode-fallback.test.ts,test/print-mode-fallback-notice.test.tsandtest/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 code-yeongyu/senpi#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
primaryErrorandfailures;lastErrorkeeps 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.