feat(agents): plan statuses and "Using your Claude plan"
P15 of the Claude/Codex plans rollout: the Claude plan's row statuses, refusals that name the plan's problem, and "Using your Claude plan" by a Claude Code agent's composer. It reuses everything P25 (#4356) put on main: the PLAN moment, setPlanStatus with its reconnect guard, limit_reached/limitResetsAt, PLAN_ROW, the limit refusal, and the composer's payer slot.
What changed
Statuses from Claude Code's stream (claude-code-moments.ts), written to the claude_setup_token row in the projection's transaction, with no new round-trip:
verified: aresultthat is not an error. It also liftslimit_reached.invalid: an assistant message whose SDKerrorisauthentication_failedoroauth_org_not_allowed. That is how the pinned SDK (0.3.304) flags a revoked or wrong token. As P25 set it up, a later turn that runs never liftsinvalid; only a new token does.limit_reached: arate_limit_eventwithstatus: "rejected", with itsresetsAt(in seconds) stored aslimit_resets_at. Anallowed_warningchanges nothing.
Refusals (claude-code-sessions.ts):
- A launch is refused in words when the row is missing,
invalid, or unreadable ("Your Claude plan needs reconnecting in Settings."). It is also refused atlimit_reachedwhile the reset it names is still ahead. The agent never switches payer. agent-plan-limit-reachednow carries an optionalresetsAt, the same wayunreachedrides onrepository-not-reachable. That covers the contractdata, the oRPC wrap,hosted-link, the client and the IPC answer schema. The composer then says "Your Claude plan has reached its usage limit. It resets Oct 22, 3:40 PM.", andmessage_agent/start_agentadd "It resets at ." to their words. Codex still answers without a reset, and its words are unchanged.
Label and Settings:
ClaudePlanLabelsits in the payer slot of aclaude_codeagent. It reads "Using your Claude plan", with the status when the plan isn't verified: "· Not yet checked", "Claude plan limit reached · Resets …", or "Claude plan needs reconnecting in Settings". As with ChatGPT, the status is read for the starter only.- The Settings row shows the same statuses.
limit_reachedstays connected (Disconnect, no Replace) and is shown in the attention colour.
Tests
runner-events-app.test.ts: one recorded stream drives unverified → verified → limit_reached (with reset) → verified → invalid → stays invalid. A warning-only limit, and an agent on an API key, leave the row alone.claude-code-sessions.test.ts: a limited plan refuses withplanResetsAtuntil the reset passes, then launches. An invalid plan refuses as signed out. No runner starts while either refuses.runtime-sessions.test.ts,handler.test.ts,coding-agent-client.test.ts: the reset goes from the steer, through the wire, to the client.coding-agent-model.test.ts,plan-label(-model).test.ts,claude-plan-section.test.ts: the words, the label and the Settings row.
Screenshots
Rendered from the real components with faked answers (a throwaway harness, not committed).
Composer label, verified · not yet checked · limit reached · needs reconnecting:


A message refused, at the limit (reset named) · on a refused token:

Settings › Coding agents › Claude Code, unverified · verified · limit reached · invalid:


Not done here
- Preview: the developer waived Preview runs for this rollout. The plan's one live Claude-plan coding task on Preview, before the cohort is turned on, is the developer's to run later.
- Typecheck: a local
tscofapps/webran out of memory in the sandbox. The packages I touched (features,service,panel) typecheck clean, and CI runs the rest.
Made with Luke