#213

Show provider retries, notices and typed failures in the transcript

Opencalledforthwants to merge intofrom

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: …":
ReasonRow offers
context_window_exceededCompact (sends /compact)
usage_limitwhen it resets, if the provider said
authentication_requiredhow to sign in on the environment's machine, then Retry
overloaded, rate_limited, server/network errorsRetry (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 CodeBecomes
system/api_retrylive retrying notice with attempt, max, cause, due time
system/status compacting / compact failedlive compacting notice / warning notice
system/compact_boundarycompacted marker with trigger and tokens before/after
system/model_refusal_fallbackmodel_fallback notice (from, to, explanation)
system/model_refusal_no_fallbackrefusal notice; the turn fails as refused
system/informational warning / suggestion, noticewarning / info notice (transcript-only info level and tool progress dropped)
system/notification medium+ priorityinfo notice, once per turn
rate_limit_event allowed_warningusage_warning notice, once per threshold, with reset time
assistant message with errorheld 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.failed gains reasons a v17 client's enum rejects, plus action and resetsAt; Turn.failure; turn.notice is now structured (noticeId, kind, retry, …); new durable turn.notice.recorded; ActivityRef.kind gains notice; history pages and thread snapshots carry notices. No new commands.
  • Migration 21. New turn_notices table sharing the thread-wide ordinal counter with messages and turn activity, so notices replay in place. turns.failure_json keeps the whole failure. turn_activity is 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
cloudflare-workers-and-pages[bot]21d ago

🚀 Deploying Preview to Cloudflare 🚀

Preview Deployments by commit

StatusDeployment URLCommitUpdated (UTC)See this deployment's details
  • Build: Failed ❌

View logs ↗
bbdd23d2026-10-09T17:10:55.149ZView logs ↗
  • Build: Failed ❌

View logs ↗
452e1a32026-10-09T12:17:44.658ZView logs ↗
  • Build: Failed ❌

View logs ↗
7f3a0662026-10-09T11:52:41.682ZView logs ↗
greptile-apps[bot]21d ago


[Critical impact] The reviewed changes appear safe to merge, with a non-blocking fix recommended for disappearing older-page gap warnings.

Findings

  1.  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

greptile-apps[bot]reviewed21d ago
greptile-apps[bot]reviewed21d ago
greptile-apps[bot]reviewed21d ago
greptile-apps[bot]reviewed21d ago
calledforthreviewed21d ago
calledforthreviewed21d ago
calledforthreviewed21d ago
calledforthreviewed21d ago
calledforthreviewed21d ago
calledforthreviewed21d ago
greptile-apps[bot]reviewed21d ago
greptile-apps[bot]reviewed21d ago
calledforthreviewed21d ago
calledforthreviewed21d ago
greptile-apps[bot]reviewed21d ago
greptile-apps[bot]reviewed21d ago