feat(chat): a usage limit pauses the queue and offers to resume at the reset

Merged
AV
10d ago
+3145-43

Walkthrough: https://app.superset.sh/page/usage-limits-in-chat-z1o65a — screenshots from the desktop dev app, including the loop end to end and the three fixes below.

Replaces #8439. #8439 was stacked on #8438, so merging it first squashed it into #8438's branch instead of main; that was reverted there (8c1947816b), #8438 landed on its own, and this PR brings the usage limit change to main by itself.

What

When the provider refuses a turn for the account's usage:

  • The transcript marks the stopped turn with one quiet line, Stopped by the usage limit, like "Stopped" for an interruption. A usage limit is a pause, not an error, so there is no red card and no actions that would hit the same limit.
  • A notice above the composer names the window (5-hour limit reached, Weekly, Weekly Opus or Sonnet, Extra usage, or "Usage limit reached"), the model, the login this chat runs on, and the reset ("Resets in 2h 14m · 5:15 AM", ticking). Its controls: the login menu (the host's logins for the agent with plan and tightest window; picking another makes it the default and reopens the chat on it, queued prompts included; plus "Usage settings"), Resume / Resume at reset (→ "Resuming at reset", click again to cancel), and dismiss.
  • The queue pauses, and prompts sent while limited wait instead of hitting the wall again. The notice is the top row of the work drawer, with the waiting prompts under a divider, so the notice, the queue and the composer read as one panel. While the notice speaks for the queue, the separate "Queue paused" bar is left out; where that bar does show for a limit, it says Queue paused by the usage limit.
  • After the reset, an armed resume waits the host's grace before it sends, and the notice counts down: Limit has reset · Resuming in 26s. It then sends the next queued prompt, or "Continue where you left off." when nothing is queued.
  • While no limit is hit, main's early warning ("5-hour limit 80% used") shows as before; the two never show together.

How

  • Protocol (@superset/chat): SessionState.usageLimit?: { hitAtMs, resetsAtMs?, resumeAtReset?, resumesAtMs?, window?, model? }, a respondToUsageLimit({ action }) command (resume | dismiss | resume_at_reset | cancel_resume_at_reset), and a usage_limit turn failure kind. All additive: older clients ignore the fields, and an unknown failure kind already falls back to "unknown".
  • ACP adapter: reads usage_update._meta (_claude/rateLimit: a rejected window not on overage, resetsAt seconds → ms, rateLimitType; and _claude/model). When the prompt is then refused it reports usageLimit before the failed turn and tags the turn error kind: "usage_limit". Legacy claude-code sessions get the same from rate_limit_event. Any harness whose turn fails with the limit in its error text gets the fallback without a reset time.
  • Live session: a limit pauses the queue; resume continues the cut-short work; resume_at_reset arms a host-side timer (reset + 30 s grace) and reports resumesAtMs; cancel clears it; dismiss drops the notice and keeps the queue paused. A dead or closed session cancels the timer.
  • Renderer: SessionView renders UsageLimitNotice as the WorkDrawer's new notice row (1 s tick during the grace countdown); the composer only hides the early warning while limited. TurnStatusRow draws a usage_limit failure as a muted line instead of the failure card. QueuePausedBar takes byLimit. AcpChatPane restarts the chat on the chosen login with its queued prompts restored.
  • 20 new strings in all 16 locales.

Verified

  • chat-runtime 426, chat 103, desktop ChatSession + ChatPane + AgentComposer 560; tsc for chat, chat-runtime, desktop and host-service; Biome; check:i18n (0 missing).
  • Regression tests for the three latest fixes fail on the code before them.
  • Redesigned after review (quiet history line, one panel): checked again live; before/after in section 6 of the walkthrough.
  • End to end in the dev app with a scripted ACP agent that refuses the way Claude does (_claude/rateLimit: rejected, then "You've hit your usage limit"): the failure card and notice, the login menu, a prompt held while limited, Resume at reset armed, the countdown after the reset, and the host sending the queued prompt 30 s later. Screenshots on the walkthrough page.
  • Not exercised: actually switching to another login (it changes the machine's default login), and a real Anthropic rate limit.

Summary by cubic

The usage limit now pauses the chat, holds queued prompts, and offers to resume immediately or at the reset, instead of failing the turn and sending queued prompts into the same wall.

Behavior

  • The failed turn shows as one muted "Stopped by the usage limit" line with no failure card or actions, like a stopped turn; a v2 turn that fails through a later error state gets the same treatment.
  • The notice leads the work drawer above the composer, naming the window, model, login, and a ticking reset countdown. The countdown is not in the live region — the title is announced once — and the row and controls wrap in narrow panes.
  • Controls are Resume, Resume at reset, dismiss, and an account switch; with nothing queued the notice is the whole panel.
  • An armed resume sends the next queued prompt (or "Continue where you left off" when empty) 30 seconds after the reset; an account switch reopens the chat with the full queue restored, hidden parts included.
  • Early-warning banners still show when no limit is hit; the two never appear together. While the notice speaks for the queue the separate "Queue paused" bar is left out; once the notice is dismissed, the bar names the limit.

Protocol

  • SessionState.usageLimit, the respondToUsageLimit command, and the usage_limit failure kind are additive, so older clients ignore them.
  • The ACP adapter reads _claude/rateLimit from usage_update._meta; the stream translator keeps refused rate_limit_events, and any harness whose turn fails with the limit in its error text gets a fallback without a reset time.

Summary by CodeRabbit

  • New Features
    • Chat sessions now recognize provider usage limits and show the affected model, reset timing, and resumption status when available.
    • Queued prompts pause when a limit is reached. Resume immediately, schedule resumption for the reset, cancel a scheduled resume, or dismiss the notice.
    • Where supported, switch accounts from the usage notice and continue the chat with queued prompts preserved.
  • Bug Fixes
    • Usage-limit stops now have a clear status message instead of a generic failure card.