fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(claude): surface usage-limit pauses in the thread - #7165

Open
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing
Open

fix(claude): surface usage-limit pauses in the thread#7165
vitalyiegorov wants to merge 4 commits into
pingdotgg:mainfrom
vitalyiegorov:fix/claude-usage-limit-surfacing

Conversation

@vitalyiegorov

@vitalyiegorovvitalyiegorov commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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. Codex gets the same treatment for its flavor of the problem: when the app-server reports codexErrorInfo: "usageLimitExceeded", the row is now a labeled quota warning carrying Codex's own reset text, instead of a generic red Runtime error.

The Claude adapter already received the SDK's rate_limit_event and forwarded it as account.rate-limits.updated telemetry, which orchestration ingestion drops. A rejected status now also emits a runtime.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 timestampFormat preference — a server-rendered 3:00 PM would 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 raw rate_limit_info still rides along as the warning's optional detail, 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:resetsAt keys — 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 no turnId.

resetsAt is 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, so projection_turns.state stays running and 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

  • 76 adapter tests pass, including new cases: one row per turn under repeated rejected events; silence for 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; unusable resetsAt keeps 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 (reading resetsAt as milliseconds would render minutes, not hours).
  • Codex adapter: 28 tests pass, including two new cases — a usageLimitExceeded error maps to a runtime.warning keeping the provider's reset text, and other errors (internalServerError) still map to runtime.error with provider_error.
  • Verified end-to-end against a real exhausted account (weekly limit hit): the SDK's genuine rejected event produced Claude 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 own You've hit your weekly limit · resets 7pm (Europe/Vienna) error text.

UI Changes

The row uses the existing runtime.warning styling (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:

WhenRow text
Reset time knownClaude usage limit reached. This turn is paused until the 5-hour limit resets in 4h 20m.
Reset time absent or implausibleClaude usage limit reached. This turn is paused until the 5-hour limit resets.
Other windowssame, with 7-day / 7-day Opus / 7-day Sonnet / overage
Window unknown to this buildClaude 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:

before: silent stall, limit discovered only via manual continue

After — same event class on this branch, against a genuinely exhausted account (no simulation): the SDK's real rejected rate-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:

after: usage-limit pause surfaced in the work log with window and reset time

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Built with Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider runtime event types for quota exhaustion (Codex consumers may have expected runtime.error) and adds nuanced Claude rate-limit gating; behavior is heavily tested but affects live thread UX during limits.

Overview
When Claude hits a rejected usage window mid-turn, the adapter now emits a runtime.warning work-log row (which limit and how long until reset) instead of leaving the thread spinning with no explanation. account.rate-limits.updated telemetry is unchanged; warnings are gated on an active turn, no provisioned overage carrying the request, and deduped per turn by rateLimitType:resetsAt so repeated SDK events and drifting countdowns do not spam the log.

Copy uses a remaining wait from epoch-second resetsAt (capped at 30 days); bad reset times still show the pause without a bogus duration. announcedUsageLimits on session context tracks what was already announced per turn, including synthetic turns.

Codex maps usageLimitExceeded error notifications to runtime.warning with the provider message instead of runtime.error.

User docs add a short FAQ for Claude stopping halfway through a turn. Extensive adapter tests cover dedupe, overage, idle-between-turns, interleaved windows, and Codex error classification.

Reviewed by Cursor Bugbot for commit c49192a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Surface Claude and Codex usage-limit pauses as runtime.warning rows in the thread

  • Claude adapter now emits one runtime.warning per unique rejected usage-limit window per active turn, via the new announcedUsageLimits field on ClaudeSessionContext that deduplicates within a turn.
  • The new describeClaudeUsageLimit helper formats the message with an optional remaining wait (e.g. in 4h 20m), capped at a maximum credible horizon; unusable resetsAt values omit the wait without dropping the row.
  • Warnings are suppressed between turns, for allowed/allowed_warning statuses, and when overage permits continuation. account.rate-limits.updated still emits for every rate-limit telemetry event.
  • Codex adapter now maps usageLimitExceeded errors to runtime.warning instead of runtime.error; other errors remain runtime.error with class provider_error.
  • Added user-facing docs in providers-claude.md explaining mid-turn usage-limit pauses.
  • Risk: ClaudeSessionContext.announcedUsageLimits must be reset per turn and per session context; if a code path reuses an old context, duplicate warnings or missed warnings may occur.

Macroscope summarized c49192a.

@coderabbitai

coderabbitaiBot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: e29d6ed1-263f-44bc-b694-cd8e6e402c14

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 16, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 16, 2026
@macroscopeapp

macroscopeappBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR changes production behavior in both Claude and Codex adapters by adding user-visible quota warnings and stateful handling for repeated limit events. The implementation remains localized and tested, but the cross-provider behavior change and nontrivial per-turn/time logic merit human review.

You can add or adjust custom eligibility rules. Learn more.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from c4d1088 to 61e2c82CompareAugust 17, 2026 05:25
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 7da225e to 7580f2cCompareAugust 19, 2026 06:35
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 7580f2c to 6168200CompareAugust 19, 2026 07:40
@macroscopeapp
macroscopeappBot dismissed their stale reviewAugust 19, 2026 07:40

Dismissing prior approval to re-evaluate 6168200

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch 2 times, most recently from 04df0c0 to ae15c15CompareAugust 19, 2026 08:18
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

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 ae15c15 by keying the dedup on the limit's identity (turnId:rateLimitType:resetsAt) rather than on the rendered row. Each has a regression test that I verified fails against the previous implementation, so they cannot silently come back.

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.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ae15c15 to e35d817CompareAugust 19, 2026 14:42
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from e35d817 to 8cbf217CompareAugust 20, 2026 04:20
@vitalyiegorov

Copy link
Copy Markdown
ContributorAuthor

Heads-up for whoever merges: the only failing check here is Vercel – t3code-marketing, which needs maintainer-approved deploys for fork branches and is currently flaking repo-wide (also failing on #7755 and #7749). This PR touches only apps/server and docs/user — no web or marketing files — so the deploy result is unrelated either way.

Everything else is green at b2d37f0: CI (Check/Test), Cursor Bugbot, Macroscope Correctness, CodeRabbit. The check is non-required (mergeStateStatus: UNSTABLE, not BLOCKED), so the PR is mergeable without waiting on the Vercel approval.

@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from b2d37f0 to 3e6a808CompareAugust 21, 2026 06:53
@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 22, 2026
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 2b5ddd9 to ffb083cCompareAugust 23, 2026 07:13

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffb083c. Configure here.

Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from ffb083c to 0f34e6dCompareAugust 28, 2026 13:18
macroscopeapp[bot]
macroscopeappBot previously approved these changes Aug 28, 2026
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>
vitalyiegorovand others added 2 commits August 30, 2026 19:14
A reviewer flagged that dual exhaustion (base window rejected and overage
also rejected) could theoretically silence the pause row, since the guard
only checks isUsingOverage/overageInUse. That specific claim doesn't hold in
practice, but the dual-rejection shape itself — the vendor's
overage-exhausted / out-of-credits scenarios — was untested. Add a
regression test alongside the sibling overageStatus: "allowed" suppression
test to lock in that this shape still surfaces the warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorovforce-pushed the fix/claude-usage-limit-surfacing branch from 54ff323 to a080cf7CompareAugust 30, 2026 17:15
Codex classifies depleted workspace credits as the same usageLimitExceeded
error code as plan limits, so the credits message already rides the
quota-warning path. Pin that with a test so it stays true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@macroscopeapp
macroscopeappBot dismissed their stale reviewSeptember 1, 2026 07:06

Dismissing prior approval to re-evaluate c49192a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Opus 5 silently stops when usage limit exceeded. Doesn't tell me immediately.

1 participant

@vitalyiegorov