feat(agents): plan statuses and "Using your Claude plan"

Merged
CH
4d ago
+832-59

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: a result that is not an error. It also lifts limit_reached.
  • invalid: an assistant message whose SDK error is authentication_failed or oauth_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 lifts invalid; only a new token does.
  • limit_reached: a rate_limit_event with status: "rejected", with its resetsAt (in seconds) stored as limit_resets_at. An allowed_warning changes 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 at limit_reached while the reset it names is still ahead. The agent never switches payer.
  • agent-plan-limit-reached now carries an optional resetsAt, the same way unreached rides on repository-not-reachable. That covers the contract data, 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.", and message_agent/start_agent add "It resets at ." to their words. Codex still answers without a reset, and its words are unchanged.

Label and Settings:

  • ClaudePlanLabel sits in the payer slot of a claude_code agent. 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_reached stays 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 with planResetsAt until 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:

Using your Claude plan Not yet checked
Limit reached with reset Needs reconnecting

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

Limit refusal naming reset Reconnect refusal

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

Settings unverified Settings verified
Settings limit reached Settings 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 tsc of apps/web ran out of memory in the sandbox. The packages I touched (features, service, panel) typecheck clean, and CI runs the rest.

Made with Luke


Devin Review