Skip to content

quota: claude read needs cross-tick memory — usage-endpoint 429 (proven) + oneshot gatherer defeats the fetcher's in-process cooldown/cache #5928

Open

Description

@dezren39

Problem

The quota gatherer's Claude read is in a near-permanent error state — Rate limited. Retrying soon. — even though the credential is valid. Verified 2026-11-04 (provider audit):

  • Direct probe: GET https://api.anthropic.com/api/oauth/usage with the stored OAuth access token (~/.claude/.credentials.json, expiresAt 2026-11-05T06:18:18Z) returns HTTP 429 {"error":{"type":"rate_limit_error",...}} — the token is accepted (a dead token returns 401, which the fetcher maps to "Claude session expired"), the endpoint is rate-limiting the read. Anthropic's OAuth usage endpoint is aggressively per-token rate-limited; Claude Code itself and the OpenChamber server poll it with the same token.
  • The OpenChamber quota module's claude fetcher already handles 429 correctly inside a long-lived process: it sets a cooldown from Retry-After and serves its in-process last-good cache. The gatherer, however, is a fresh oneshot process every ~15 minutes, so the cache and cooldown reset every tick: every tick pays a fresh endpoint call (adding to the very pressure that trips the limit), and no last-good read can ever be retained — there is no successful Claude percentage read in the gatherer's snapshot history through this path.

Fix shape (aggregator side, pm-stack)

Give the gatherer cross-tick memory for the claude provider, mirroring the fetcher's own design:

  • Persist { lastGoodEntry, cooldownUntil } for the claude read in the gatherer state file (quota-gatherer-state.json).
  • On a tick: if now < cooldownUntil and a last-good entry exists, emit it marked retained (the existing markRetained machinery) and skip the endpoint call entirely.
  • On a fresh 429: set cooldownUntil = now + Retry-After (fall back to the fetcher's 5 min default, cap 60 min) and emit the last-good entry retained when one exists; only surface the bare error when no read has ever succeeded.
  • On success: store the entry as the new last-good.

Acceptance: with the endpoint in its current 429 mood, consecutive ticks stop calling it every 15 min, the board's Claude row carries a retained last-good (once one exists) instead of a raw error, and the heartbeat distinguishes "in cooldown, retained" from a fresh failure. Keep the change scoped to the claude run; other providers' finalize/retention behavior (#5719) is unchanged.

Related: #5704 (WS6 gatherer fixes), #5721 (gatherer lib source).

Acceptance criteria

  • The Claude quota read no longer sits in a near-permanent Rate limited. Retrying soon. state across gatherer ticks when the endpoint is not actively limiting.
  • Cooldown/cache state survives across oneshot gatherer invocations (cross-tick memory on disk or equivalent), so the fetcher's in-process cooldown is actually honoured.
  • A 401 response still maps to 'Claude session expired' (dead-token path unchanged); a 429 maps to a bounded backoff that other pollers' activity does not reset.
  • Demonstrated over multiple consecutive ticks: state file/cache is written on a limited read and read back on the next tick.

Activity

  1. developing-today commented on Nov 5, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/highImportant and time-sensitive
    effort/mMedium: expected under 1 day of work
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Nov 5, 2026
  3. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    Intake review

    Related: Proven by direct probe 2026-11-04 (provider audit): the usage endpoint returns 429 for the stored OAuth token while Claude Code and the OpenChamber server poll the same endpoint. Sibling quota work: openchamber issue #1619.

    Still-live check: searched merged PRs referencing this issue at 2026-11-05T04:31:57Z — none found; the issue stands as filed.

    Notes / risks: The endpoint is shared with other pollers; a fix that polls harder makes the fleet's rate position worse. Any cross-tick store must not leak the OAuth token into logs or world-readable files.

    Suggested approach: Persist the fetcher's cooldown/cache in a small state file beside the quota output, keyed by credential fingerprint, and have the oneshot gatherer load it before fetching and save it after.

    Acceptance criteria were appended to the issue body and labels normalised by the fleet monitor's intake pass.

  4. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 5, 2026
  5. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    Agent note - handed off

  6. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  7. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    Triage — 2026-11-05 (current main: 5ca5cc8)

    Verdict: still open; not superseded. Fresh issue read shows OPEN with no linked PR. GitHub PR/commit searches for #5928 found no matching work. The current-main advance over the local checkout is #6326, an unrelated CI workflow change.

    Verified path and scope correction: the oneshot service/timer runs every 15 minutes. pm-stack/aggregator/scripts/gather-quota.ts invokes opencode quota --json on each tick; Claude is included in that unfiltered provider listing. The current producer is therefore the OpenCode CLI path, not the OpenChamber Claude provider whose in-process cache the issue body describes. The OpenCode Claude provider currently fetches the usage endpoint directly and maps 429 to “Rate limited. Retrying soon.”; the CLI result does not carry Retry-After/cooldown metadata. Generic retention in the gatherer can preserve a last-good row after a failed read, but does not suppress the next endpoint call. quota-gatherer-state.json stores snapshot dedupe stamps, not provider cooldown state, and is only rewritten when new snapshot records are appended.

    Fix scope: suppress Claude upstream of the fetch (the no-argument CLI listing fetches configured providers, so provider selection/exclusion must be resolved before assuming the aggregator can skip it); persist bounded cooldown and last-good state across oneshot invocations, including when no snapshot advances; expose a distinct retained/in-cooldown heartbeat result. Preserve the 401-to-session-expired behavior, propagate or otherwise obtain bounded Retry-After/fallback timing, and never persist raw credentials. Add a two-invocation test for 429 → cooldown skip/retained → expiry/retry, plus 401 and legacy-state behavior. Relevant aggregator seams include pm-stack/aggregator/scripts/gather-quota.ts, pm-stack/aggregator/src/model.ts and quota gatherer tests; CLI/provider selection may require the OpenCode source as well. Existing tests cover generic retention, not cross-process request suppression.

    Ordering: task 10485 for #5721 is currently handed to an OpenCode session and also edits gather-quota.ts. To respect one-writer-per-file, hold #5928 implementation until #5721's PR lands, or explicitly combine/coordinate the changes; do not edit that file in parallel. #5721's runtime repair is documented separately; this is only the active source-file collision.

    Issue remains open and is parked for that ordering dependency.

  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    status/triagedTriaged and routed (see agent note)
    and removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

effort/mMedium: expected under 1 day of workkind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/highImportant and time-sensitivequeue/parkedQueue task is parked (waiting on CI, time, or an event)status/parkedDeliberately paused with a recorded resume conditionstatus/triagedTriaged and routed (see agent note)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions