Skip to content

[BUG] Prompt suggestions silently suppressed whenever the client-derived rate-limit status is allowed_warning — strict-equality gate in Vxy #72495

Description

@inth3shadows

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 SDK
prompt_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 being
utilization >= 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.status is the derived status
  • a prompt_suggestion message 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 to
allowed 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

  1. allowed_warning is 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).
  2. 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.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:tuibugSomething isn't workinghas reproHas detailed reproduction stepsplatform:linuxIssue specifically occurs on Linux

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions