Skip to content

Active quota probe: poll the account usage endpoints so meters exist before the first turn #207

Description

@EstarinAzx

Parent

Spec #201. Follow-up from the #204 recon spike, whose verdict was build — see the evidence comment on
#201. Deliberately not ready-for-agent: the three decisions in "Open calls" below are human scoping,
and #201 states the passive header side-channel stays the source of truth for this release, so this lands
after 2.0.43.

What to build

An active quota lane: read the account usage endpoint on a cadence, cache the reading, and let the
statusline show real numbers before the session's first turn — the hole the passive header
side-channel cannot fill, since headers only arrive on a response.

Both endpoints are verified to answer Wisp's stored OAuth credentials with no extra scope or consent:

  • Anthropic — GET https://api.anthropic.com/api/oauth/usage, Authorization: Bearer <stored token>,
    anthropic-beta: oauth-2025-04-20
  • Codex — GET https://chatgpt.com/backend-api/wham/usage, Authorization: Bearer <stored token>,
    chatgpt-account-id: <stored account id>

(/backend-api/api/codex/usage is a 404 — do not carry it forward.)

Both agree with the header-derived meters exactly, so this is a second door onto the same ledger, not a
competing truth.

Hard constraints (from the recon — do not rediscover these)

  1. The Anthropic endpoint carries a multi-minute 429 penalty. Never call it on the statusline render
    path; the statusline runs constantly. Cached, low-frequency poll only, renderer reads the cache.
  2. Parse limits[] on shape, never the named top-level keys. The payload carries seven_day_opus,
    seven_day_sonnet, seven_day_cowork, seven_day_oauth_apps plus unstable codenames (tangelo,
    iguana_necktie, nimbus_quill, omelette_promotional, cinder_cove, amber_ladder), nearly all
    null. status.ts already matches on shape for this exact reason.
  3. Normalize units at the boundary. This endpoint reports an integer percent and ISO-8601 resets_at;
    the header path reports a 0..1 fraction, and status.json stores epoch seconds — which Statusline: expired quota window renders refilled, not alarmed #203's
    expired() predicate depends on.
  4. Redact on values. Codex returns email, user_id, account_id; Anthropic returns an org uuid and a
    workspace id in response headers. None of it may reach status.json, bridge.log, or the repo.
  5. Codex secondary_window is null on a Plus plan — consistent with the settled one-window rule; do not
    render an absent window as 0%.

What it unlocks beyond the current two numbers

Model-scoped weekly windows (scope.model.display_name), which window is actually binding (is_active),
severity, extra-usage/credit state, spend caps, plan_type, and Codex's authoritative limit_reached /
rate_limit_reached_type.

Open calls (human, before this is agent-ready)

  • Cadence — how often, and is it wall-clock or event-driven (session start, post-turn, on stale cache)?
  • Precedence — when a poll and a turn header disagree, which wins? They agreed exactly in the recon, so
    a "freshest wins" rule is probably enough, but it is a decision, not a default.
  • Opt-in or always-on — this is an extra outbound call against the user's account on their credential.

Acceptance criteria

  • Cadence, precedence, and opt-in decided and written into the ticket before implementation starts.
  • Poll is cached and never fires from the statusline render path.
  • limits[] parsed on shape; no meter invented from a null named bucket.
  • Units normalized to the existing QuotaMeter contract (integer percent, epoch-second resetAt).
  • No account-identifying value reaches status.json, bridge.log, or the repo.
  • A statusline opened before the first turn of a session shows real meters.

Blocked by

None mechanically — blocked on the three open calls above and on 2.0.43 shipping.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions