[BUG] Prompt suggestions silently suppressed whenever the client-derived rate-limit status is allowed_warning — strict-equality gate in Vxy
Update 2026-07-23 — I have located the gate in the shipped binary, captured
the suppression live, and confirmed it with a pre-registered prediction (the
derived status cleared itself at the predicted time and suggestions resumed with
no user action — see "Prediction, then observation" below). Replacing my original
reproduction steps, which described only one of the two ways to reach the warned
state.
Root cause
From the 2.1.218 bundle (byte-identical in 2.1.216 and 2.1.217):
function Vxy(e){
if(!e.promptSuggestionEnabled) return "disabled";
if(e.pendingWorkerRequest||e.pendingSandboxRequest) return "pending_permission";
if(e.elicitation.queue.length>0) return "elicitation_active";
if(e.toolPermissionContext.mode==="plan") return "plan_mode";
if(mie().status!=="allowed") return "rate_limit";
return null
}
mie().status is one of allowed | allowed_warning | rejected. The strict
inequality means the still-fully-usable allowed_warning state is treated
exactly like rejected.
Its only caller is the suggestion generator, which bails and emits telemetry:
let l=r(), c=Vxy(l);
if(c) return s7(c,void 0,o), null; // tengu_prompt_suggestion {outcome:"suppressed", reason:c}
Both entry points reach it, so this affects the TUI ghost text and the SDKprompt_suggestion message identically:
Mnd(e,t)— interactive,querySource.startsWith("repl_main_thread"), source"cli"- the print/SDK path — source
"sdk"
Two routes into allowed_warning (this is what my original report missed)
Txu(headers) re-derives the status rather than passing the header through.
When the header is allowed or allowed_warning, it calls zzg(); if that
returns a warning object the derived status is allowed_warning, otherwise it is
normalized down to allowed.
zzg warns on either a -surpassed-threshold header, or this pace table:
Uzg=[
{rateLimitType:"five_hour", claimAbbrev:"5h", windowSeconds:18000,
thresholds:[{utilization:0.9, timePct:0.72}]},
{rateLimitType:"seven_day", claimAbbrev:"7d", windowSeconds:604800,
thresholds:[{utilization:0.75,timePct:0.6},
{utilization:0.5, timePct:0.35},
{utilization:0.25,timePct:0.15}]}
]
with Gzg(resetsAt, windowSeconds) returning the fraction of the window
elapsed, and the rule beingutilization >= f.utilization && elapsedPct <= f.timePct.
That is an ahead-of-pace test, not a near-the-limit test. On the weekly claim
it fires at 25% used in the first 15% of the week, 50% in the first 35%, or 75%
in the first 60%. A user who front-loads their week is suppressed for days while
every request still succeeds.
Live capture — 2026-07-23 08:04 EDT, v2.1.218, Linux
Server response headers on /v1/messages:
anthropic-ratelimit-unified-status = allowed
anthropic-ratelimit-unified-7d-status = allowed
anthropic-ratelimit-unified-7d-utilization = 0.62
anthropic-ratelimit-unified-7d-reset = 1785218400
anthropic-ratelimit-unified-5h-utilization = 0.53
anthropic-ratelimit-unified-representative-claim = five_hour
Client-derived state, from the stream-json rate_limit_event:
{"status":"allowed_warning","resetsAt":1785218400,
"rateLimitType":"seven_day","utilization":0.62,"isUsingOverage":false}
The server said allowed. The client stamped itself allowed_warning because
0.62 >= 0.5 and only 0.3219 of the week had elapsed (window opened
2026-07-21 02:00 EDT).
Client telemetry from the same run (base64 additional_metadata decoded):
tengu_prompt_suggestion_init -> {"subscription_type":"max","enabled":true,"source":"env"}
tengu_prompt_suggestion -> {"subscription_type":"max","source":"sdk",
"outcome":"suppressed","reason":"rate_limit",
"prompt_id":"user_intent"}
Enablement passed. The runtime gate suppressed with reason: "rate_limit".
No prompt_suggestion message was emitted on any probe turn.
For completeness, my earlier report was based on a June capture in which the
header itself read allowed_warning — so both routes are real and both land
on the same gate.
Reproduction — headless, no TUI, no waiting for a usage window
CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=1 \
claude -p "say the word apple, nothing else" \
--output-format stream-json --verbose --prompt-suggestions
CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=1 \
claude -p --continue "now say the word banana, nothing else" \
--output-format stream-json --verbose --prompt-suggestions
Two turns are required — the generator early-returns early_conversation below
two assistant messages. Then read the second run's output:
rate_limit_event.rate_limit_info.statusis the derived status- a
prompt_suggestionmessage present = fired; absent while the derived status
is allowed_warning = suppressed by this gate
Prediction, then observation (2026-07-23)
Ahead of time I predicted: with the 7d claim ahead of pace, the rule{utilization:0.5, timePct:0.35} stops matching the moment window-elapsed crosses
0.35 — 2026-07-23 12:48:00 EDT — after which the derived status returns toallowed and suggestions resume with no user action, no restart, no settings
change.
Observed, via three unattended headless probes on the same machine and session:
| probe (EDT) | derived status | representative claim | 7d utilization | prompt_suggestion emitted |
|---|---|---|---|---|
| 12:35 (before) | allowed_warning | seven_day | 0.70 | no |
| 12:55 (after) | allowed | five_hour | — | yes |
| 13:30 (confirm)| allowed | five_hour | — | yes |
The derived status flipped across the predicted 12:48 boundary and suggestions
returned on the next turn — nothing on my end changed between 12:35 and 12:55.
That is the gate clearing itself as the pace rule stopped matching, exactly as the
code path predicts.
Note that 7d utilization actually rose from 0.62 (08:04) to 0.70 (12:35) over
the morning, yet suppression still cleared — because 0.70 never reached rule 1's
0.75 threshold, so only the {0.5, 0.35} rule was ever armed, and that rule is
released by elapsed time (crossing 0.35 of the window), not by usage falling. This
is the clearest possible confirmation that the trigger is pace, not proximity to
the limit: I was using more quota when it came back.
What I think should change
allowed_warningis a fully usable state — requests succeed. Suppressing on it
should be a deliberate, documented decision, not a strict-equality fall-through.
If it is deliberate, gate on rejected (or on the derived warning only above a
much higher utilization).
- If it stays, surface it. The suppression is completely silent — the reason
exists in telemetry (reason: "rate_limit") but is shown nowhere to the user.
Five closed issues here are people chasing env vars and settings for a state
the client already knows the reason for.
- Consider whether a pace heuristic — 62% used at day 2.3 of 7 — should govern
an ambient UI hint at all, when the header itself says allowed.
Tracked separately as a feature request in #72497.
Related: #57822 (same strict-equality class, fixed), #79919 and #77144 (may be
this gate or may be the separate enablement gate — see my comment there for a
one-command discriminator), #30699 (tengu_crystal_beam has zero occurrences in
2.1.218, so the budgetTokens: 0 theory there no longer applies).
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
See https://github.com/anthropics/claude-code/issues/79919#issuecomment-5054186146
Update 2026-07-28 — still present in 2.1.220, plus a live capture of the suppression and the missing second route.
Three things below: (1) the gate is intact two releases later, with the full call chain re-derived from the shipped 2.1.220 binary; (2) a mechanical capture of the suppression happening, including the route my original report did not cover; (3) a note on the cross-reference in the comment above, which I believe points at a different bug.
---
1. The gate is intact in 2.1.220
The function was renamed (
Vxy->dOy) but the logic is unchanged. Verbatim from~/.local/share/claude/versions/2.1.220:Gie()returns the module-level status object (wpe, initialised to{status:"allowed",...}). The strict inequality is still what does the damage:allowed_warningis a fully usable state and is treated identically torejected.The suppression telemetry is also unchanged:
So every occurrence is already being reported server-side as
outcome:"suppressed", reason:"rate_limit". That telemetry should make the population size directly measurable without needing to reproduce anything.Reported against 2.1.207; confirmed on 2.1.216, 2.1.217, 2.1.218, and now 2.1.220. Sixteen patch releases.
2. There are two routes to
allowed_warning, and they are checked in orderMy original write-up described the pace table but not the header route, and the ordering matters for anyone trying to reproduce this. Re-derived from 2.1.220:
OZgis only consulted when the server's own status is non-fatal:Route A —
PZg, the-surpassed-thresholdheader. If the server sendsanthropic-ratelimit-unified-<claim>-surpassed-threshold, the client returnsallowed_warningimmediately, carrying asurpassedThresholdfield.Route B —
MZg, the client-side pace table. Unchanged from what I quoted before:with the rule
c>=f.utilization && d<=f.timePct, whered=LZg(u,i)is the fraction of the window elapsed. This is an ahead-of-pace test, not a near-the-limit test — front-load your week and you are suppressed for days while every request still succeeds.A field-level tell for triage: route A's return value includes
surpassedThreshold; route B's does not. That single field distinguishes "the server warned us" from "the client decided we were ahead of pace" in any capturedrate_limit_event, with no extra instrumentation.3. Live capture of the suppression — 2026-07-28 21:31:51 EDT, 2.1.220, Ubuntu 24.04 (WSL2)
Non-interactive, one turn:
The emitted
rate_limit_event:Full census of stream message types in that run:
prompt_suggestion: 0. Every request in the run succeeded;statusisallowed_warning, notrejected.Decoding the window against the capture time:
Both routes were satisfied simultaneously here: route A because the server sent
surpassedThreshold: 0.9, and route B becauseutilization 0.95 >= 0.90andelapsedPct 0.5729 <= 0.72. Route A returned first by the ordering inOZg, which is whysurpassedThresholdis present in the payload. Worth noting that the two are not mutually exclusive, so a fix must address both.What this run does and does not establish. It establishes that the client entered
allowed_warningwhile fully usable, and that no suggestion was emitted. It does not by itself isolate the gate as the cause, because a run inallowed_warningcannot distinguish "suppressed bydOy" from "print mode never emits". Separating those needs a control run atstatus: "allowed", which I cannot produce until the current window resets. I will post the control rather than leave the inference dangling.That said, the print/SDK path clearly does emit this message type when not gated —
prompt_suggestionis a first-class stream message in the bundle:and the stream consumer branches on it explicitly (
if(l.type==="prompt_suggestion")continue;). The--prompt-suggestionsflag is documented as "In print/SDK mode, emits a prompt_suggestion message after each turn", and the CLI rejects it unless--print --output-format=stream-jsonare also passed.4. On the cross-reference above
The comment above points to #79919's server-rollout analysis. I do not think that explains this issue, for a reason visible in that thread's own evidence: the
rate_limit_eventposted there readsstatus: "allowed".dOyonly returns"rate_limit"when the status is notallowed, so that reporter was never on this path. That reading is also no longer the position of its author, who subsequently replaced it with an expired-OAuth-credential theory in the same thread.For completeness, on the credential theory: I can find no branch in either gate that consults token state.
dOytestsdisabled,pending_permission,elicitation_active,plan_mode,rate_limit; the downstream generator addsaborted,early_conversation, andlast_response_error. No expiry or refresh-token check appears in either. That may still be a real third bug, but it is not this one, and merging the three would lose the distinct fixes.5. Suggested fix, unchanged
Treat
allowed_warningas usable:The state is not rate-limited by any behavioural definition — every request in the capture above succeeded. If suppression at high utilisation is deliberate, it should be a distinct reason code so the telemetry can tell an intentional throttle apart from this fall-through, and the pace-table route should be reconsidered independently, since firing at 25% of the weekly claim inside the first 15% of the week is not a proximity-to-limit signal at all.
Correction to an earlier version of this comment: I previously wrote that the user at least sees a "You're close to your usage limit" banner in this state. That is wrong below 70% utilisation, and the reason is worth stating on its own — see the
fLu/dOyasymmetry in my follow-up comment below. At 26% weekly there is no banner and no suggestion, i.e. no signal of any kind.---
Environment: Ubuntu 24.04 (WSL2), Anthropic API, 2.1.220 native install,
promptSuggestionEnabled: true, not in plan mode,defaultMode: auto. All symbol names above are from the 2.1.220 build (BuildID 788318c9115981678ca1a25f40cdb3b39df71403) and are minifier-assigned, so they will differ between builds; the string literals and numeric thresholds are stable and greppable. No emoji or non-ASCII glyphs in any quoted output.Edited: this comment previously led with a probe-methodology digression. The claim in this issue does not depend on any probe, and centring one obscured that. Rewritten to state what is verifiable from the shipped binary, plus a cross-issue prediction that others can check.
The claim is a code fact, not a measurement
dOyin 2.1.220, verbatim:Gie()returns the client-derived rate-limit state. Once that state isallowed_warning, suggestions are off. Deterministically, for every caller. This needs no reproduction — it is what the shipped code does.There is no user-facing way to turn it back on
This is the part I want to put on the record, because it is what makes the issue actionable rather than cosmetic.
CLAUDE_CODE_ENABLE_PROMPT_SUGGESTIONresolves throughqwo():Setting it to
1forces the result true. But that value feedspromptSuggestionEnabled, which is gate 1 ofdOy. The rate-limit check is gate 5, and its expression —Gie().status!=="allowed"— reads nothing the user can set. No env var, nosettings.jsonkey, no CLI flag, no/configtoggle appears anywhere in that condition.So a user can explicitly force the feature on and still get silence, with no error and no indication why. That is by construction, not by accident.
allowed_warningis reached at genuinely low usageMeasured on this machine, 2.1.220, and independent of anything to do with suggestions:
No
surpassedThresholdfield, so this came from the client-side pace table, not a server header. Exactly one threshold can produce it:| threshold |
util >= X|elapsed <= T| fires ||---|---|---|---|
| 0.75 / 0.60 | 0.26 >= 0.75 no | -- | no |
| 0.50 / 0.35 | 0.26 >= 0.50 no | -- | no |
| 0.25 / 0.15 | 0.26 >= 0.25 yes | 0.12937 <= 0.15 yes | yes |
26% of the weekly claim consumed, 12.9% of the week elapsed, every request succeeding, status
allowed_warning. The pace rule is an ahead-of-pace test, not a near-the-limit test, so front-loading a week trips it at a quarter of the claim and keeps it tripped for days.A prediction that connects this to #77144 and #79919
Both of those threads have users reporting that suggestions vanished and stayed gone, that
CLAUDE_CODE_ENABLE_PROMPT_SUGGESTIONwas explicitly set to1ortrue, thatpromptSuggestionEnabled: truewas insettings.json, and that restarts and reinstalls changed nothing. The working theory there is a server-side rollout drop.Gate ordering above predicts exactly that symptom without any rollout: the env var satisfies gate 1, gate 5 still returns
"rate_limit", and the user sees a silently dead feature that no local setting revives.Two ways to tell the explanations apart, neither requiring code access:
R7emitstengu_prompt_suggestion {outcome:"suppressed", reason:...}with the specific gate name.rate_limit,early_conversation,disabled, andemptyare distinct reason codes. For any affected account, the reason is already recorded server-side — no reproduction needed from anyone.For @Neur0Support and @HarryMuc: if suggestions ever come back without you changing anything, that is this bug rather than a rollout, and worth noting on #77144.
Suggested fix, unchanged
allowed_warningis a fully usable state — every request in the capture above succeeded. If suppression at high utilisation is deliberate, it needs (a) a distinct reason code so it is separable from this fall-through in telemetry, and (b) some user-visible signal — today there is none at all below 70% utilisation, for the reason set out in the addendum below. The pace-table route deserves separate reconsideration regardless: firing at 25% of the weekly claim inside the first 15% of the week is not a proximity-to-limit signal.---
Environment: Ubuntu 24.04 (WSL2), Anthropic API, 2.1.220 native install (BuildID
788318c9115981678ca1a25f40cdb3b39df71403). Minified symbol names are build-specific; string literals and numeric thresholds are stable and greppable. No emoji or non-ASCII glyphs in any quoted output.---
Addendum: why this produces sharp cutoffs, at window resets
The pace rule is
utilization >= X && elapsedPct <= T, whereelapsedPctis the fraction of the window already gone.elapsedPctis at its smallest immediately after a window resets — so the ahead-of-pace test is easiest to satisfy right after the rollover, which is precisely when a user has the most headroom and expects the fewest restrictions.Translating the weekly thresholds into wall-clock:
| weekly threshold | suppressed for the first... |
|---|---|
|
utilization >= 0.25| 25.2 h of the window (elapsed <= 0.15) ||
utilization >= 0.50| 58.8 h (elapsed <= 0.35) ||
utilization >= 0.75| 100.8 h (elapsed <= 0.60) |A user who consumes a quarter of the weekly claim in the first day is suppressed for that day, then it clears on its own. Nothing about that is visible to them.
The gate is not new. I checked the shipped binaries for 2.1.200, 2.1.207, 2.1.218, 2.1.219 and 2.1.220: all five contain the same
if(<status>!=="allowed")return"rate_limit"check and a byte-identical threshold table (0.9/0.72on 5h;0.75/0.6,0.5/0.35,0.25/0.15on 7d). So a sudden onset is not explained by the rule appearing in an update — the rule was already there, and what changes is which side of it your usage falls on.A falsifiable prediction, and what would settle it
If this gate is the cause of a "suggestions died and never came back" report, the suppression is periodic, not permanent:
A server-side rollout drop is monotonic. An expired credential is monotonic. Only this one cycles. So a single observation of suggestions returning on their own, mid-window, with nothing changed, distinguishes them.
This matters for #77144 and #79919, where the reported pattern is a sharp cutoff plus
CLAUDE_CODE_ENABLE_PROMPT_SUGGESTIONset explicitly and having no effect -- which is exactly what gate ordering predicts, since that variable can only satisfy gate 1.What I have not established, stated plainly: I cannot confirm those reports are this bug. I do not know their utilisation at cutoff, and I cannot see their window boundaries. Two specific cautions against over-reading the correlation:
/logincannot separate "the credential fix worked" from "the window happened to cross" -- if the true cause is window-crossing, any action taken near the ~25 h mark correlates perfectly.Three things would settle it, in descending order of ease for Anthropic:
R7emitstengu_prompt_suggestion {outcome:"suppressed", reason:...}, andrate_limit,early_conversation,disabled,emptyare distinct codes. For any affected account the answer is already recorded -- no reproduction required from anyone.anthropic-ratelimit-unified-5h-surpassed-thresholdinto a response would drive the client toallowed_warningon demand and A/B the gate directly, rather than waiting on real usage. Trivial for anyone with control of the response path; I have not run it.---
The strongest form of this: the banner and the suggestion gate disagree
fLurenders the usage banner from the same rate-limit state object thatdOygates suggestions on:The banner has a 70% sanity floor. The suggestion gate has none.
| path | input | result |
|---|---|---|
|
fLu(usage banner) |allowed_warning,utilization: 0.26|0.26 < 0.7-> no banner ||
dOy(suggestions) |allowed_warning| -> suppressed |So at the 26% weekly reading captured above, the user gets no banner and no suggestions -- no signal of any kind that a feature was turned off, and nothing to connect it to usage.
The codebase has already made the judgement that
allowed_warningbelow 70% is not worth surfacing to the user, because a "you're close to your usage limit" warning at 26% would be absurd.dOyacts on that same state anyway. Whatever the right threshold for suppression is, these two functions reading one variable should not disagree about whether it means anything.A minimal fix that respects the existing judgement, if suppression at genuinely high utilisation is intended:
or, if the warning state should still suppress, at least apply the same floor
fLualready uses, so the two agree.On measuring this from outside
For anyone trying to reproduce: do not use the presence or absence of a
prompt_suggestionevent as your detector. It has a high false-negative rate independent of this bug. Two further gates sit ahead ofdOyin the same chain --early_conversation(below two assistant messages) andcache_cold(input + cache_creation + output > 10000, which fires on the first turn after any idle gap once the prompt cache has expired) -- and downstream, generated suggestions are dropped by a content filter that reports them asempty. In four consecutive headless runs atstatus: "allowed"with everything else clear, I observed zero suggestions.The
rate_limit_eventline, by contrast, is emitted on every run and is a direct read of the state variable this issue is about. That is the thing worth reporting.