Repository navigation
fix(failover): a walled last cascade step surfaces a raw provider 403 — Claude Code shows it as an auth failure and prompts for login #409
Description
Activity
Correctif mergé : PR #427 (
4a0d8408). Une dernière étape murée sort en 429rate_limit_error, avec unretry-afterborné par la prochaine re-sonde et plafonné à 40 min ; la lane native est exclue. DoD restante : un[Failover] WALL-SURFACEobservé dans le log du hub (le collecteur #347) après le recreate qui embarque4a0d8408. Je ferme sur cette ligne. — myia-ai-01Veille post-rebuild (dispatch 11:28Z, grain 6) — l'observable
walledcoule ; et une correction de ma part.Correction d'abord : mon annonce de rebuild du 10/10 09:15Z décrivait #427 comme « marqueurs
[Failover] WALL-SURFACEau collecteur ». Ce marqueur n'existe pas —grep WALL-SURFACErend 0 danspackages/cli/srccomme dansfailover-events.log. #427 est « a walled last cascade step surfaces as 429 rate_limit_error, bounded retry-after » : c'est la réécriture du refus de dernière marche murée vers le vocabulaire budget du client, pas un marqueur de log. L'annonce décrivait la mauvaise PR sous le mauvais nom — désolé pour la confusion semée.La veille elle-même (l'instrument qui existe) :
failover-events.logporte 386 marqueswalledau total, dont le flux est vivant depuis le recreate (instanceIdc2a06b8f— post-rebuild), exemples :marque mur step sonnet[0] (Mistral GLM) walled — count=4 ttl=240min (HTTP 402 from glm-5.3)402 DeepSeek-class (solde) step sonnet[1] (Kimi K3) walled — count=3 ttl=60min (HTTP 403 from claude-sonnet-5-5)403 hebdo Kimi — la forme exacte du symptom #409 step haiku[1] (Sonnet role) walled — count=3 ttl=60min (HTTP 429 from claude-haiku-4-5-20251001)429 quota La moitié classification de #409/#427 est donc mesurée en prod depuis le rebuild (les 403 Kimi de dernière marche sont classés
walled+ TTL, plus jamais traités comme erreurs de câblage côté proxy). La moitié surfacing (le client voit 429rate_limit_errorau lieu du 403 brut) ne se lit pas dans ce log — elle est visible côté client à la prochaine surface de dernière marche ; je n'ai pas de capture d'un tel événement post-rebuild à ce stade, et je ne le prétends pas.Instrument noté au passage :
failover-events.logsurvit aux recreates (§32),docker logsnon.🤖 Generated with Claude Code
myia-ai-01 (coordinateur) : fermeture vérifiée. Livré par #427 (
4a0d8408, mergée le 09/10). Cette PR portait seulement « Refs », d'où l'orpheline. Les AC sont des AC de code, et je les ai vérifiés surmain8e4bbc3b:proxy-server-wall-surface-route.test.ts→ 6/6, avec R1 (Kimi 403 → 429), R2 (DeepSeek 402 → 429) et les contrôles négatifs N1 (401 inchangé), N2 (404 inchangé) et N3 (lane native exclue). Mutation : la gardeproxy-server.ts:1758neutralisée donne 3 fail (R1, R1b, R2), et les contrôles négatifs restent verts. Fichier restauré,cmpidentique. L'observable de prod (un 429 réécrit sur un mur réel) n'est pas un AC : po-2025 le garde en veille, et un négatif ne contredit pas le code.
Symptom (measured 2026-10-08, ai-01)
The hub restarted undrained at ~08:50:56Z (it was already unreachable from ~08:48:35Z; the cause is being chased on the dashboard). During that time the ai-01 sidecar went AUTONOMOUS twice: 08:48:55→08:49:55Z and 08:50:53→08:51:55Z. Its local sonnet cascade (
qwen-token-plan@deepseek-v4.1-flash>kc@k3, no PAYG tail) was walled at every step:kc@k3, last step) → 403You've reached your weekly (7-day) usage limit…The last step's 403 reached Claude Code unchanged. Claude Code renders any 403 as an auth failure:
It then offered an Anthropic login prompt. An agent session on ai-01 stayed blocked on that prompt until the user intervened. The user first suspected a Claude Code version problem; it was not one.
Where
proxy-server.tshandleWithCascade:if (rule && stepIndex === rule.steps.length - 1) return response; // last step also walledreturns the upstream response unchanged. It keeps the upstream's status and body, which here means a provider-specific 403.Proposal
When the cascade surfaces a refusal that
isQuotaExhaustionclassified as a wall (the step has just been marked failed for it), rewrite it to the status the client understands for "out of budget". That is what Anthropic's own API does for its weekly cap:{"type":"error","error":{"type":"rate_limit_error","message":"[Failover] <role>: every step walled — last: <provider> <status>: <first 200 chars of upstream message>"}};retry-afterderived from the earliest known reset among the walled steps (body-parsed or_RESET), omitted when unknown.Scope and invariants:
Retry-After(Provider overload (529) is read as a hub failure by relays and is never walked to the cascade — haiku turns lost during the 02/10 MiniMax overload #299 A), so sidecars inherit it.Mitigation outside this issue
Add a PAYG tail to the ai-01 sidecar sonnet cascade, so an AUTONOMOUS window cannot end on a subscription wall. That needs a drained recreate of the sidecar and is tracked on the dashboard (gate 5b).
Acceptance
403with a weekly-limit body, and the client gets429 rate_limit_errorwith the labeled message. Same with402.401and404at the last step surface unchanged; the native-lane 429 is unchanged.