fix(claude): surface usage-limit pauses in the thread - #7165
fix(claude): surface usage-limit pauses in the thread#7165vitalyiegorov wants to merge 2 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces new user-facing behavior by showing Claude usage-limit warnings in the thread when turns are paused. While well-tested and following existing patterns, new user-visible messaging warrants human review to validate UX decisions. You can add or adjust custom eligibility rules. Learn more. |
c4d1088 to
61e2c82
Compare
7da225e to
7580f2c
Compare
7580f2c to
6168200
Compare
Dismissing prior approval to re-evaluate 6168200
04df0c0 to
ae15c15
Compare
|
Understood — this is a routing call rather than a defect, so flagging the state for whoever picks it up. The earlier verdict's blocking line ("1 blocking correctness issue found at or above your repo's Minimum Blocking Severity") is gone: both Medium findings on the dedup — the countdown-drift duplicate and the synthetic-turn silence — are fixed in To make the human review as small as possible, the description now lists every string this change can put in front of a user — four variants of one sentence, each pinned by an assertion. The whole user-visible surface is one work-log row, at most once per turn per window, emitted only while a turn is genuinely parked; no new components, tones, or notifications. One transparency note on the screenshot: it was captured against a genuinely exhausted account and shows the wording from before the copy change, when the row rendered an absolute reset time. That copy now renders as a wait, for the reason described in the body — the server would otherwise bake its own timezone and locale into a row read on other machines, which #6190 and #7081 previously fixed elsewhere. I could not re-capture, since that needs another real exhausted window; the strings above are the current output and are asserted in the suite. |
ae15c15 to
e35d817
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e35d817. Configure here.
e35d817 to
8cbf217
Compare
|
Heads-up for whoever merges: the only failing check here is Everything else is green at |
When a Claude subscription window closes mid-turn, the SDK emits a rate_limit_event with status "rejected" and then parks the turn until the window reopens: no more messages, no result, no turn.completed. The adapter turned that event into an account.rate-limits.updated telemetry event, which ingestion drops on the floor, so the thread just spun with no explanation (pingdotgg#6513). The rejected event now also emits a runtime.warning naming the window and its reset time, which ingestion already turns into a thread activity row and web and mobile already render as a warning line in the timeline. The notice is deduped per turn, since sibling fields in the rate-limit payload drift while the window is parked and re-fire the event with an identical rendered line; "allowed" and "allowed_warning" stay quiet. resetsAt is epoch seconds, as the CLI's own formatter confirms, and a value that lands outside the Date range renders without a time rather than throwing a RangeError that would kill the session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b2d37f0 to
3e6a808
Compare

What Changed
When Claude Code hits a subscription usage limit mid-turn, the thread now shows a warning row saying which window ran out and when it resets, instead of silently spinning.
The Claude adapter already received the SDK's
rate_limit_eventand forwarded it asaccount.rate-limits.updatedtelemetry, which orchestration ingestion drops. Arejectedstatus now also emits aruntime.warning— the same mechanism the adapter already uses for high-priority CLI notifications — which ingestion turns into a thread activity row that web and mobile already render as a warning line. No contract, schema, migration, or client changes.The row states the remaining wait ("resets in 4h 20m"), not a wall-clock time. This code runs on the server while the row is read on clients that may sit in another timezone and locale, and that carry their own
timestampFormatpreference — a server-rendered3:00 PMwould be wrong for exactly the remote setups T3 Code is built for, and would reintroduce the implicit-locale default that #6190 and #7081 removed. A wait reads the same everywhere, needs no contract or client change, and the row's own client-formatted timestamp says when the wait started. The rawrate_limit_infostill rides along as the warning's optionaldetail, so a future client-side renderer has the exact instant with no work now.The notice is deduped on the limit's identity — each turn carries a set of
window:resetsAtkeys — not on the rendered row. A parked window re-fires as its sibling fields drift, and the remaining wait shrinks between those repeats, so keying on the text would emit a fresh row about once a minute. A turn can park on more than one window, so the set (rather than a single slot) keeps an interleaved repeat of an earlier window from re-announcing. The set is replaced whenever the turn id changes, so every new turn — including a synthetic one auto-started for a background agent — announces its pause again with no extra bookkeeping.Three conditions gate the row, so it never claims a pause that isn't happening: the status must be
rejected, the account must not be carrying the request on provisioned overage (overageStatus/isUsingOverage), and a turn must actually be in flight — the SDK stream stays live between turns, where "this turn is paused" would be false and would persist an orphan row with noturnId.resetsAtis epoch seconds; an absent or implausible value (more than 30 days out) renders the row without a wait rather than a bogus one.Why
Fixes #6513. The SDK parks the turn until the window reopens without emitting a result or
turn.completed, soprojection_turns.statestaysrunningand the UI spins with zero indication. Users only discover the cause by typing "continue" and getting the limit error back — in the real thread that motivated this fix, that blind spot lasted 11 hours.This deliberately does not add a turn watchdog or change turn/session state — it only surfaces the pause. Making the parked turn resolve itself is a separate concern.
Verification
allowed/allowed_warning/malformed payloads while a turn is genuinely in flight; silence between turns and when overage is carrying the request; one row when a parked window repeats five minutes later (the countdown drifts, the identity does not); a fresh row for a synthetic turn parked on the same window; one row per window when two windows interleave inside a turn; unusableresetsAtkeeps the session alive; a retried turn re-announces the pause. The wait assertion is locale- and timezone-independent and pins the seconds-to-milliseconds scale (readingresetsAtas milliseconds would render minutes, not hours).rejectedevent producedClaude usage limit reached. This turn is paused until the 7-day limit resets at Aug 16, 7:00 PM GMT+2.— exactly matching Claude's ownYou've hit your weekly limit · resets 7pm (Europe/Vienna)error text.UI Changes
The row uses the existing
runtime.warningstyling (warning icon + tone) in the work log; no new UI components were added.Every string this PR can put in front of a user, all pinned by assertions in
ClaudeAdapter.test.ts:Claude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.Claude usage limit reached. This turn is paused until the 5-hour limit resets.7-day/7-day Opus/7-day Sonnet/overageClaude usage limit reached. This turn is paused until the limit resets in 4h 20m.That is the whole surface: one row, one sentence, at most once per turn per window. Nothing else changes about what users see — no new components, tones, sounds, or badges — and the row is emitted only while a turn is actually parked.
Before — a production thread where Claude hit the weekly limit mid-work. The turn died silently with a generic runtime error at 20:29; the reason only surfaced ~11 hours later when the user manually sent "continue" and got the limit error back:
After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real
rejectedrate-limit event now renders a labeled work-log row the moment it arrives, and its reset matches Claude's own error text below it. Note the capture predates the copy change described above: it shows the earlier wall-clock wording (resets at Aug 16, 7:00 PM GMT+2) where the branch now renders the equivalent wait (resets in 4h 20m). The row, its trigger, and its styling are otherwise unchanged, and re-capturing needs another genuinely exhausted window:Checklist
Built with Claude Fable 5 in Claude Code.
🤖 Generated with Claude Code
Note
Low Risk
Adapter-only UX for existing SDK rate-limit events via the existing
runtime.warningpath; no auth, contracts, or turn-state changes.Overview
When Claude hits a subscription usage limit mid-turn, the thread now shows a runtime warning instead of spinning silently. The adapter already forwarded
rate_limit_eventas telemetry; arejectedwindow now also emits one work-log row per distinct limit while a turn is actually parked.The row names the window (5-hour, 7-day, etc.) and remaining wait (
resets in 4h 20m), not a wall-clock time. Dedup is onwindow:resetsAtper turn so repeats with a shrinking countdown do not spam, interleaved windows each get a row, and a new or synthetic turn re-announces. Allowed, overage-carried, idle, and malformed events stay off the work log; unusableresetsAtdrops the wait suffix without killing the session.Reviewed by Cursor Bugbot for commit 3e6a808. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Emit
runtime.warningfor rejected Claude usage-limit pauses inClaudeAdapterrate_limit_eventhandling inClaudeAdapterthat emits oneruntime.warningper distinct rejected limit window during an active turn, with a human-readable wait duration derived fromresetsAtannouncedUsageLimitsonClaudeSessionContext; allowed, overage-allowed, malformed, and between-turn events stay quiet whileaccount.rate-limits.updatedtelemetry still fires for every eventCLAUDE_USAGE_LIMIT_WINDOWSlabels andCLAUDE_USAGE_LIMIT_MAX_WAIT_MS(30 days) cap for rendering waits; unusable or missingresetsAtvalues drop the wait suffix without failing the sessionruntime.warningwill see these additional eventsMacroscope summarized 3e6a808.