fix(usage): surface the Claude account rate limit (session/week) - #143
Merged
Conversation
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.
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_pctshape. The real_meta._claude/rateLimit(confirmed againstclaude-agent-acp@0.44.0source) is the SDK's flatrate_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 onstatus: "rejected". The gauge badge turns red when limited. HandlesresetsAtin seconds or ms, and self-heals (clears once the reset time passes).2. The snapshot never persisted when it mattered
_claude/rateLimitrides a cost-lessusage_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→ newlastRateLimit/lastRateLimitAt) the instant it arrives, decoupled from the ledger and deduped against the last seen value.getMyAgentUsageprefers 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/rateLimitshape is upstream-defined — worth a quick confirm against live staging data once an agent exercises a real limit.