# Core and Web App: transport and quota errors reconnect with a visible countdown, auth failures lock the Session recoverably

Two field failures used to abort the turn outright with `[Aborted]: llm request error: …` — a dropped connection (`terminated: other side closed (UND_ERR_SOCKET)`) and a gateway quota rejection (`403 … no active subscription (insufficient_user_quota)`). Neither is a verdict on the Session: a socket drop is just the network, and a quota error heals when the balance tops up. Both now go through the engine's in-run reconnect (the `[turn_retried]` flow that re-sends the turn's input with the already-produced content attached).

The classifier got honest about error shapes: retryability probes the `cause` chain (Node's `fetch` wraps the real transport failure — `UND_ERR_SOCKET` and friends live on `cause`) and the parsed provider body in its OpenAI and Anthropic SDK nestings; a bare `terminated` only counts alongside transport vocabulary, so "terminated by content filter" copy does not retry. The quota carve-out stays tight — 402/403 with `insufficient_user_quota` / `insufficient_quota` codes or a message naming quota/subscription — and **authentication signals are checked first**: a definitive auth code can never be swallowed by the quota keyword heuristic into the retry path.

Retries follow one shared exponential ladder, `min(250ms × 2^(n−1), 30s)`, up to **8 reconnects (~62s of total patience)** — the first two attempts are exactly as fast as before, so a transport blip still recovers in under a second, while the tail is long enough for a quota window to actually close. Compaction requests keep their own cap of 3 (~1.75s): a failed compaction already preserves the original context and retries on the next trigger, so failing fast beats stalling the session. While a longer wait runs, the retry line shows a **live countdown** (whole seconds, anchored on the client clock, driven by the new additive `retry_in_ms` on `request_end` — computed by the same formula as the actual sleep) with two inline controls: **Retry now** (`POST /api/sessions/:id/retry-now`, skips the remaining wait without consuming an attempt) and **Give up** (the ordinary abort). `request_end` also carries the failure detail (`message`), so the Cost center's errors panel records the real cause — the 403 body, not a generic "timeout" — even when retries follow.

Authentication errors don't retry; they surface as a sixth `StopReason` value — `auth` on the outcome and on the streamed `request_end.status` (the abort event stays reason-only; no new fields) — and the composer locks — **recoverably**. The Session pins its model *reference* at creation, but credentials are read from the current Project config on load, so fixing the key on the Models page is the actual remedy: the models update now invalidates every cached runtime in the Project (previously only vault edits did — a fixed key could sit unused for up to the 30-minute idle sweep) and broadcasts a `credentials_updated` event, unlocking open tabs instantly. Across reloads the lock is a time gate keyed on the last main-session `request_end("auth")` timestamp — dead only while that failure is newer than the credentials' file mtime — so a fixed key stays fixed, a still-wrong key re-locks on its next failure, and any later successful request clears the state too. The notice's primary action opens the Models page, with Retry and New Session as escapes, and the stuck draft stays selectable. A subagent's auth failure never locks the parent. End-to-end proof: a quota 403 retries through a visible, decreasing countdown (with Retry-now firing early on click), and a 401 locks the composer, unlocks the moment the key is updated via the API with no click required, and stays unlocked after a reload.
