Back to Ruflo

ADR-312: Usage-limit downtime prevention (Cognitum meta-LLM fallback CTA)

v3/docs/adr/ADR-312-usage-limit-downtime-prevention.md

3.26.111.9 KB
Original Source

ADR-312: Usage-limit downtime prevention (Cognitum meta-LLM fallback CTA)

Status: Proposed — Phase 0 buildable now; Phase 1 blocked on an upstream Claude Code capability that does not exist today Date: 2026-07-10 Relates to: ADR-301 (promotional status surface), ADR-303 (credit-exhaustion experience), ADR-311 (remote message feed)

Context

The user wants: when a Claude Code session is approaching or has hit its usage/rate limit (the kind of event that produces a UI banner like "You've used 97% of your session limit · resets 6:10pm"), ruflo should detect this and surface a message in the statusline directing the user to cognitum.one's meta-LLM proxy — framed as downtime prevention / session extension: keep working on ruflo/Claude Code via a local or hosted alternative model until the Anthropic-side limit resets. This is opt-in, gated by existing consent (ADR-302 proxy-install / cloud-routing domains), and would use the same remote-message + click-tracking plane ADR-311 already built.

The 97% example the user gave was illustrative, not literal — the ask is the general capability: detect approaching-or-hit usage limits and act on it.

Research performed this session (grounded, not assumed)

Per direct instruction, the installed Claude Code CLI source was inspected directly — not just documentation — to determine whether this signal is actually available to a script like ruflo's statusline hook.

Installed version inspected: @anthropic-ai/[email protected] at /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js (minified bundle, 17,466 lines / ~59K-char lines).

Finding 1 — the signal is real and exists inside Claude Code's process

Grepping the bundle for rate-limit-related identifiers turned up a live, actively-used internal state object (module-level variable, referred to here by its minified name Lv — this WILL rename on every Claude Code release since the bundle is unstably minified):

js
Lv = {
  status: "allowed" | "allowed_warning" | "rejected",
  resetsAt?: number,                    // epoch SECONDS (confirmed: formatter does `new Date(q*1000)`)
  rateLimitType?: "five_hour" | "seven_day" | "seven_day_opus" | "seven_day_sonnet" | "overage",
  utilization?: number,                 // 0.0–1.0 float; UI does Math.floor(u*100) for the %
  unifiedRateLimitFallbackAvailable: boolean,
  isUsingOverage: boolean,
}

rateLimitType maps to exactly the limit types real users see: "five_hour""session limit" (this is what produced the 97% banner), "seven_day""weekly limit", "seven_day_opus" / "seven_day_sonnet" → per-model weekly limits, "overage""extra usage".

The literal banner text the user saw is generated by a UI-only string formatter reading exactly this object:

`You've used ${pct}% of your ${limitName} · resets ${resetTimeStr}`

confirmed via direct string match in the bundle.

Finding 2 — the data is sourced from real Anthropic API response headers

The state object is populated by parsing actual HTTP response headers on /v1/messages calls:

anthropic-ratelimit-unified-status
anthropic-ratelimit-unified-reset
anthropic-ratelimit-unified-fallback
anthropic-ratelimit-unified-representative-claim
anthropic-ratelimit-unified-overage-status
anthropic-ratelimit-unified-overage-reset
anthropic-ratelimit-unified-overage-disabled-reason
anthropic-ratelimit-unified-upgrade-paths
anthropic-ratelimit-unified-{type}-utilization   (per rateLimitType)

This confirms the signal is genuine server-side rate-limit telemetry, not a client-side heuristic Claude Code invented.

Finding 3 — this state is NOT exposed through any current extension point

Checked exhaustively, with empirical verification (not just documentation reading):

Extension pointResult
statusLine stdin JSONConfirmed absent. The only fields present (verified against ruflo's own statusline.cjs parser, which already reads this payload) are model.display_name, context_window.used_percentage, cost.total_cost_usd, cost.total_duration_ms. No rate-limit field exists in this schema today.
Hook events28 hook event names exist in the bundle (PreToolUse, PostToolUse, Notification, SessionStart, SessionEnd, Stop, PreCompact, etc.). None is documented or confirmed to fire on a rate-limit status change. A Notification hook exists generically but its trigger conditions for this specific case could not be confirmed from the bundle in the time available — worth testing empirically, not confirmed either way.
Debug logsEmpirically checked: ~/.claude/debug/latest (an existing 11,424-line debug log from a real prior session) contains zero occurrences of anthropic-ratelimit, even at the default debug log level. The headers are read and discarded internally; they are not logged even in the debug artifact.
CLI command / MCP resourceNone found.
React UI hook (v96())Exists, but it's an internal React hook wired into Claude Code's own ink/TUI rendering — not reachable from outside Claude Code's process.

Conclusion

Detecting Claude Code's actual usage-limit state from outside its process is not currently possible through any documented or discovered mechanism. The signal exists and is real, but Claude Code does not surface it. This is a hard blocker on the literal ask ("detect the usage limit"), not a ruflo implementation gap.

This finding should NOT be treated as permanent — Anthropic could add this to the statusLine schema or a hook payload in a future release, and the exact identifiers above will very likely be renamed/reshuffled on any Claude Code version bump (this is a minified bundle inspected for research purposes, not a stable API ruflo should build automated tooling directly against).

Decision

Ship this in three phases. Phase 0 is real, useful, and buildable today. Phase 1 is a documented dependency on Anthropic shipping a capability ruflo cannot build itself. Phase 2 is the actual downtime-prevention UX, unblocked once Phase 1 lands.

Phase 0 — ship now: opt-in, self-reported "I'm rate limited" signal

Since ruflo cannot detect the real signal, give the user (or a wrapper script they control) an explicit way to flip a local state flag when they notice the banner themselves:

bash
ruflo settings notices rate-limited        # user manually flags: "I just got rate-limited"
ruflo settings notices rate-limited --clear # clears it (also auto-clears after a TTL)

This reuses the exact credit-notifier.ts pattern already shipped in ADR-311/ADR-303 (markCreditExhausted / clearCreditStatus / *Notice()) — a new sibling module rate-limit-notifier.ts with:

  • markRateLimited(rateLimitType, resetsAt?, now) — persists ~/.ruflo/rate-limit-status.json
  • clearRateLimitStatus(now)
  • rateLimitNotice(now) — user-facing single line, same shape as creditExhaustedNotice()
  • A TTL (default 6h) so a stale manual flag doesn't linger forever if the user forgets to clear it

The statusline promo row, when this flag is set, overrides normal rotation with a dedicated CTA (server-served per ADR-311 — a new class: 'rate-limit' message class, same content pipeline, same click-tracking, same "zero local content" discipline) pointing at cognitum.one's meta-LLM proxy setup flow.

This is honest about being manual/self-reported — it is NOT automatic detection, and should be labeled as such in any UI copy (e.g. ruflo settings notices rate-limited help text should say "tell ruflo you've hit a Claude usage limit" not "ruflo detected...").

Phase 1 — blocked on upstream: request the real signal from Claude Code

File the concrete ask (as a Claude Code feature request / GitHub issue against anthropics/claude-code, not something ruflo can implement unilaterally): expose the internal rate-limit state via either:

  • (a) A new field on the existing statusLine stdin JSON, e.g.:
    json
    { "rate_limit": { "status": "allowed_warning", "type": "five_hour", "utilization_pct": 97, "resets_at": 1783720200 } }
    
  • (b) A new hook event fired on rate-limit status transitions, e.g. RateLimitStatusChanged, with the same payload shape.

Either is sufficient; (a) is simpler for Claude Code to ship since the statusLine hook already re-renders on a fast cadence and the data already exists in the process that spawns it.

Do not build speculative detection against the current minified bundle's internal variable/function names (Lv, di1, xI4, etc.) — those are not a stable interface, will silently break on any Claude Code version bump, and reverse-engineering them further to build a production dependency would be fragile in a way that actively hurts users when it breaks (a stale/wrong rate-limit banner is worse than none). This research exists to ground THIS ADR's decision, not to become load-bearing production code.

Phase 2 — once Phase 1 lands: the real downtime-prevention UX

With a real, Anthropic-sanctioned signal available:

  1. Detectionrotation.ts (or a new rate-limit-gate.ts) reads the real rate_limit field from the statusline JSON each render.
  2. Threshold logic — surface the CTA when status === "allowed_warning" (approaching) OR status === "rejected" (hit). Different copy for each (per the existing messages.ts class discipline — new classes rate-limit-warning / rate-limit-exhausted, remote-served, same content pipeline).
  3. Priority override — a rate-limit CTA should preempt the normal 4:1 educational:promotional rotation (this is directly analogous to how ADR-303's credit-exhaustion recovery surface already overrides normal rotation — reuse that precedent, don't invent a new priority system).
  4. Consent gate — the CTA itself is fine to show unconditionally (informational: "you're rate limited, here's an alternative") since it's not tracking anything by showing it; the click still routes through the existing consent-gated attribution/click-tracking pipeline (ADR-311 §7) exactly like any other promo.
  5. Event vocabulary — add rate_limit_warning_shown / rate_limit_exhausted_shown to the closed event set (types.ts FunnelEventName), following the exact pattern promo_impression/promo_open already established.
  6. Auto-clear — once status returns to "allowed" in a subsequent render, the override lifts automatically; no manual clear needed (Phase 0's manual flag becomes redundant once Phase 1 ships, but should stay as a fallback for any user manually invoking it in the interim, or for other agent-harness integrations that don't have Phase 1 data).

Consequences

  • Phase 0 ships a real, if manual, capability today — no dependency on Anthropic, reuses proven ADR-303 patterns, small diff.
  • The literal ask ("detect approaching or met") is NOT achievable automatically without Phase 1. This ADR is explicit about that rather than shipping a fragile heuristic (e.g., scraping stdout for the banner string, or guessing from context_window.used_percentage — which measures a completely different thing, context window fill, not usage/rate limit) that would silently produce wrong signals and erode trust exactly the way ADR-301's copy discipline was designed to avoid.
  • This ADR should be revisited the moment Claude Code ships any version of Phase 1's data — at that point Phase 2 is a small, mechanical extension of the existing rotation/promo/click-tracking machinery, not a new subsystem.

References