Skip to content

fix(usage): surface the Claude account rate limit (session/week) - #143

Merged
DIodide merged 2 commits into
stagingfrom
fix/surface-account-rate-limit
Jun 21, 2026
Merged

fix(usage): surface the Claude account rate limit (session/week)#143
DIodide merged 2 commits into
stagingfrom
fix/surface-account-rate-limit

Conversation

@DIodide

@DIodide DIodide commented Jun 21, 2026

Copy link
Copy Markdown
Owner

A user on their own Claude subscription hit the session limit — Harness chat showed "You've hit your session limit · resets 6:20am (UTC)" but the Usage dialog showed nothing, so they had no clue. Two bugs (the agent message is the agent's own text; the structured data was being missed):

1. Wrong parse shape

The frontend guessed a buckets.five_hour.utilization_pct shape. The real _meta._claude/rateLimit (confirmed against claude-agent-acp@0.44.0 source) is the SDK's flat rate_limit_info: {rateLimitType, status, resetsAt, utilization?, …} describing the single most-restrictive window. Rewrote the parser to that shape; the "Claude account limits" section now shows the window label, a % bar when present, a reset countdown, and a red "limit reached" banner on status: "rejected". The gauge badge turns red when limited. Handles resetsAt in seconds or ms, and self-heals (clears once the reset time passes).

2. The snapshot never persisted when it mattered

_claude/rateLimit rides a cost-less usage_update, and a hard-limit turn fails silently (no result message) — so the snapshot only ever rode a cost-bearing usage row and was dropped exactly at the limit. Now it's persisted directly onto the credential (agentCredentials.recordRateLimit → new lastRateLimit/lastRateLimitAt) the instant it arrives, decoupled from the ledger and deduped against the last seen value. getMyAgentUsage prefers that account-level snapshot.

Process

Source investigation (claude-agent-acp@0.44.0) → fix → adversarial review (3 dims × skeptic) caught a medium sticky-banner regression + 2 lows — all fixed (self-heal on expired reset, || budget fallback, write dedup).

Validation

biome clean · tsc 21/21 baseline · FastAPI 323 · Convex 195 · web 234. New tests: Convex (snapshot surfaces with zero usage; owner-gated), frontend (rate_limit_info shapes incl. rejected, utilization, seconds/ms reset, self-heal on past reset).

Note: the _claude/rateLimit shape is upstream-defined — worth a quick confirm against live staging data once an agent exercises a real limit.

DIodide added 2 commits June 21, 2026 01:44
When a user on their own Claude subscription hit the session limit, Harness
showed nothing in the Usage dialog — they had no clue. Two bugs:

1. Wrong parse shape. The frontend parser guessed a `buckets.five_hour.…`
   shape; the real `_meta._claude/rateLimit` is the SDK's flat `rate_limit_info`
   ({rateLimitType, status, resetsAt, utilization?}). Rewrote the parser to that
   shape (confirmed against claude-agent-acp@0.44.0 source) and the "Claude
   account limits" section to show the window label, %, a reset countdown, and a
   red "limit reached" banner on status=rejected. The gauge badge turns red when
   the account limit is hit. Handles resetsAt in seconds or ms.

2. The snapshot never persisted at the moment it mattered. `_claude/rateLimit`
   rides a cost-less `usage_update` (rate_limit_event), and a hard-limit turn
   fails silently with no result message — so the snapshot only ever rode a
   cost-bearing usage row and was dropped exactly when the limit was hit. Now
   persist it directly onto the credential (`agentCredentials.recordRateLimit`,
   new `lastRateLimit`/`lastRateLimitAt`) the instant it arrives, decoupled from
   the usage ledger; getMyAgentUsage prefers that account-level snapshot.

Tests: +2 Convex (snapshot surfaces with zero usage; owner-gated), +5 frontend
(rate_limit_info shapes: rejected, utilization, type labels, seconds/ms reset,
null cases). FastAPI 323, Convex 195, web 234; biome clean, tsc 21/21.
- [MED] The "limit reached" banner was sticky: the credential snapshot is
  preferred unconditionally and only updates on a status change, so after the
  window passively reset the red "turns are paused" banner stayed (false).
  accountUsageFromRateLimit now returns null once resetsAtMs is in the past —
  the section + gauge self-heal on the next render.
- [LOW] UsageBadge level uses `||` not `??` so a 0% account utilization can't
  suppress a real Harness budget %.
- [LOW] The gateway now only persists the rate-limit snapshot when it actually
  changed (dedup vs session.last_rate_limit) — no credential-row write on every
  repeat usage_update.

+1 self-heal test (past reset → null); active-limit tests use future timestamps.
FastAPI 323, web 11 (usage-display), biome clean, tsc 21/21.
@DIodide
DIodide merged commit dbb6a90 into staging Jun 21, 2026
4 checks passed
@DIodide
DIodide deleted the fix/surface-account-rate-limit branch June 21, 2026 16:44
@claude

claude Bot commented Jun 21, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant