kind — never its prose message — chooses whether the user is sent to Settings, offered Retry, or shown no action at all.
Quick Start
1
See a 401 (auth)
An expired or wrong API key returns
kind: "auth". The error row offers Go to Settings, because a retry with the same rejected credential just fails again.2
See a 502 (transport)
A dead socket or a proxy returning an HTML error page classifies as
kind: "transport". The error row offers Retry — the engine may be fine and the next attempt may reach it.The Recovery Mapping
recoveryFor(kind) is exhaustive over ErrorKind. Every kind lands in exactly one of three affordances.
The message text is prose from a provider and may say anything. The recovery button is chosen from
kind alone, so a reworded provider error never silently loses its affordance.Pre-first-token and mid-stream are unified
A failure before the first token now surfaces the same real reason as one mid-stream. Both reach the error row with their truekind.
transcript.ts::apply() promotes an error that arrives while the turn is still idle: it flips the turn to streaming and reprocesses the event, so the reason survives. Before this, every pre-first-token failure was dropped and re-labelled kind: "empty", so a 401 and a dead socket rendered identically — and the auth → settings branch was unreachable.
turn at the top of runTurn, so an error arriving without a start event is applied to the new turn rather than dropped as wrong_msg_id against the previous turn’s ended state.
start drop rule.
Common Pitfalls
The default baseUrl is the phone itself
The default baseUrl is the phone itself
The default
baseUrl is 127.0.0.1:8765, which on a phone resolves to the phone — not your dev machine. With nothing listening there, the very first turn fails with a refused connection. Set the engine address in Settings before the first send.Expired credentials vs. offline
Expired credentials vs. offline
An expired key is
auth and routes to Settings; being offline is transport and routes to Retry. They are distinct kinds precisely so the app does not tell an offline user to fix credentials that are fine.A 403 is auth, not transport
A 403 is auth, not transport
A
403 — what a scoped key or a proxy returns — is kind: "auth", so it sends the user to credentials rather than offering an endless Retry.Before this fix, the second-turn failure was silent
Before this fix, the second-turn failure was silent
Turn 1 answered, turn 2 got a 401, and the screen still showed turn 1’s answer with turn 1’s
end outcome. The composer cleared on Send, but the user got no error row, no auth prompt, and no hint a run was attempted. Silent, repeatable, permanent.If you integrate against pre-4572 mobile builds, expect the failure to appear only after the next successful turn — carrying a “1 event could not be read” row blaming itself for its predecessor. The default baseUrl is 127.0.0.1:8765 — the phone itself — so “engine unreachable” is the common case, not an exotic one.Related
Approvals & Cancellation
Stopping a run, and what a refused stop does.
Dropped Events
The before-
start drop rule and the error exception.Boot Failures
Failures that never reach a turn at all.
Protocol
The
error event and its kind field on the wire.Chat Recovery
A corrupt chat file surfaced without hiding the rest.

