What
Provider problems stop showing up as plain chat lines. Retries, compactions, model switches, refusals, usage warnings and failures now arrive typed and render as their own transcript rows:
- Live notices (retrying with attempt n of m and the cause, compacting) show as one shimmering line at the end of the running turn and go away once the turn moves on. They are never stored.
- Durable notices (compacted, model switched after a refusal, refused, approaching a usage limit, provider info/warning) are stored, come back with history and snapshots, and sit where they happened in the turn.
- Failures carry a typed reason and a recovery action instead of "Turn failed: …":
| Reason | Row offers |
|---|---|
context_window_exceeded | Compact (sends /compact) |
usage_limit | when it resets, if the provider said |
authentication_required | how to sign in on the environment's machine, then Retry |
overloaded, rate_limited, server/network errors | Retry (re-sends the failed turn's prompt and images) |
refused | "Try rephrasing the request." |
Only the newest turn's failure offers its button. A failed turn now keeps its failure across a reload; before, the failure line vanished.
Why
Claude Code reports most of this structurally, but we dropped it: api_retry became a generic "recovering" notice, compactions were only logged, and Claude Code's own synthetic assistant message ("API Error: 529 …", "You've hit your limit …") was streamed as if the model had said it. The SDK flags that message with SDKAssistantMessage.error, so it is now held back from the reply and turned into the turn's typed failure (or a warning if the turn recovers).
How Claude maps
| Claude Code | Becomes |
|---|---|
system/api_retry | live retrying notice with attempt, max, cause, due time |
system/status compacting / compact failed | live compacting notice / warning notice |
system/compact_boundary | compacted marker with trigger and tokens before/after |
system/model_refusal_fallback | model_fallback notice (from, to, explanation) |
system/model_refusal_no_fallback | refusal notice; the turn fails as refused |
system/informational warning / suggestion, notice | warning / info notice (transcript-only info level and tool progress dropped) |
system/notification medium+ priority | info notice, once per turn |
rate_limit_event allowed_warning | usage_warning notice, once per threshold, with reset time |
assistant message with error | held back; types the failure with terminal_reason, api_error_status and the last rate_limit_info (reset time) |
ACP providers keep working: recoverable errors become a generic retrying notice, auth errors offer sign-in guidance, and prompt errors whose message is unmistakable (rate limit, usage limit, context length, overloaded) are typed. Everything else fails as a plain provider error, as before.
Wire and storage
- Protocol v18.
turn.failedgains reasons a v17 client's enum rejects, plusactionandresetsAt;Turn.failure;turn.noticeis now structured (noticeId,kind,retry, …); new durableturn.notice.recorded;ActivityRef.kindgainsnotice; history pages and thread snapshots carrynotices. No new commands. - Migration 21. New
turn_noticestable sharing the thread-wide ordinal counter with messages and turn activity, so notices replay in place.turns.failure_jsonkeeps the whole failure.turn_activityis untouched. - Assumes the tool-payloads PR merges first. It takes protocol 17 and migration 20. This branch carries a placeholder migration 20 (
reserved_for_tool_payloads, a no-op) only so the catalog stays contiguous; replace it with the real one when rebasing.
Known, not changed here
While a client re-hydrates a thread, it buffers durable records but applies transient frames (turn.notice) at once (environment-client websocket.ts). That predates this PR. On the server, transient thread events now take the same microtask queue as stored ones, so they leave in production order. A notice that lands during re-hydration can still be applied before buffered output it followed, and that output then clears it early. The effect is cosmetic: a retry line goes away a moment too soon.
Screenshots (Storybook, App/TurnNotices)
| Retrying | Compaction |
| Durable notices | Overloaded |
| Context window | Usage limit |
| Sign in | Older failure, no button |
The assets/turn-notices-shots branch only holds these images; delete it after merge.
Tests
- protocol: 283 passed (new
turn-notices.test.ts: kinds per event, text caps, durable cursor, failure reasons/actions, snapshot and history notices) - agent-runtime (via desktop vitest): 714 passed across desktop + runtime (new
claude-problems.test.ts,problems.test.ts, projectAgentEvent mappings, runtime and ACP cases) - server: 832+ passed (new
turn-notices.test.ts: transient vs stored, ordinal placement, typed failure persisted, late-join snapshot and replay; migration 21 test; query plan for the notice page) - environment-client: 263 passed (live notice lifecycle, durable placement, snapshot/history merge, failure on the turn)
- app-core: 463 passed (wording, projection, rendering, Retry and Compact through the app)
- web: 310 passed
- typecheck: every package; eslint and prettier on touched files
Server cli/lifecycle fail in a shell that inherits the live server's OPENMANAGER_TUNNEL_HOSTNAME (a "Tunnel token missing" warning on stderr); they pass with it unset. App-core boundary.test.ts timed out once under full-suite load and passes alone.
🤖 Generated with Claude Code
Activity
26[Critical impact] The reviewed changes appear safe to merge, with a non-blocking fix recommended for disappearing older-page gap warnings.
Findings
- Older gap warnings disappear ▶
Summary
This PR adds typed provider notices and failures throughout the transcript, storage, replay, and recovery controls.
- Provider retries and problems become typed notices and failures.
- Durable notices and failures return in their place in turn history.
- The transcript shows provider notices and recovery steps as their own rows.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
Provider[Provider event] --> Runtime[Typed notice or failure]
Runtime --> Live[Live retry or compaction notice]
Runtime --> Stored[Durable notice or failure]
Live --> Queue[Ordered socket delivery]
Stored --> SQLite[SQLite history and event log]
SQLite --> Queue
SQLite --> Reload[History or reconnect snapshot]
Queue --> Client[Client thread state]
Reload --> Client
Client --> Transcript[Transcript rows and recovery buttons]Reviews (3) · Last reviewed commit: "Address review round 2 on turn notices" · Reviewed by Greptile
🚀 Deploying Preview to Cloudflare 🚀
Preview Deployments by commit
View logs ↗
View logs ↗
View logs ↗