Skip to content

fix(usage): harden subscription-usage fetch (review follow-ups to #150)#152

Merged
DIodide merged 1 commit into
stagingfrom
fix/agent-usage-review
Jun 22, 2026
Merged

fix(usage): harden subscription-usage fetch (review follow-ups to #150)#152
DIodide merged 1 commit into
stagingfrom
fix/agent-usage-review

Conversation

@DIodide

@DIodide DIodide commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Follow-ups from an xhigh code review of #150 (10 finder angles → verify → sweep). Applies the surviving findings.

Fixed

# Severity Fix
1 med (CONFIRMED) Scope the fetch to claude-code. The elif cred_id: branch fired for any agent with a linked credential; _fetch_subscription_usage hardcodes "claude-code", so a codex/cursor cred raised AgentCredentialsError (caught) and logged a stack trace ~once/min while streaming, plus a wasted Convex round-trip. Now elif cred_id and session.agent_id == "claude-code".
2 med Empty {} snapshot. if rl is not None treated a present-but-empty dict as real data → blocked the fetch and persisted {} over a good snapshot. Now if rl: (empty falls through to the fetch, never persisted).
3 low Dedupe the buckets write. Mirrors the flat path — only recordRateLimit when the snapshot changed; no redundant Convex write every ~60s on a long session.
4 low Bound the debounce map. _sub_usage_fetched_at was never pruned; now trimmed past the TTL once it grows large.
5 low Stable React keys. Account-limit bars keyed on the human label → collision if two windows fall back to the generic "Claude account" label. Now keyed on a stable window id.

Skipped (by design, not bugs)

  • The /v1/messages ping that consumes a sliver of quota — it's the only way to read the unified rate-limit headers (count_tokens omits them; /api/oauth/usage needs a user:profile scope the inference token lacks). Debounced to ≤1/min.
  • The wait-out-the-TTL-on-failure debounce — intentional, avoids hammering a failing endpoint.
  • The two-sources-one-lastRateLimit-field clobber — latent (the SDK currently emits nothing, so only the fetch writes); the empty-{} + dedupe + claude-code-scoping fixes cover the reachable cases.

Tests

FastAPI 361 pass; frontend 240 pass (added a key-collision regression test); tsc baseline, biome clean.

🤖 Generated with Claude Code

Addresses findings from an xhigh review of #150:

- Scope the fetch to claude-code: `elif cred_id and session.agent_id ==
  "claude-code"`. It was firing for ANY agent with a linked credential —
  _fetch_subscription_usage hardcodes "claude-code", so a codex/cursor cred
  raised + logged a stack trace (caught) roughly once a minute while streaming.
- Treat an empty `{}` rate-limit snapshot like an absent one (`if rl:` instead
  of `is not None`): an empty dict no longer blocks the fetch or clobbers a
  good stored snapshot.
- Dedupe the buckets write: only persist when the fetched snapshot changed
  (mirrors the flat path) — no redundant Convex write every ~60s on a long
  session.
- Bound `_sub_usage_fetched_at`: prune entries past the TTL once it grows large,
  so the per-credential debounce map can't grow unbounded on a long-lived process.
- Key the account-limit bars on a stable window id, not the human label, so two
  windows that fall back to the generic "Claude account" label can't collide.

Skipped by design: the /v1/messages ping (the only way to read the unified
rate-limit headers — count_tokens omits them, /api/oauth/usage is scope-blocked)
and the wait-out-the-TTL-on-failure debounce (intentional, avoids hammering).
@DIodide DIodide merged commit b7418e8 into staging Jun 22, 2026
4 checks passed
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