Repository navigation
Open
Labels
effort/mMedium: expected under 1 day of workMedium: expected under 1 day of workkind/bugSomething isn't working (prefixed form of the classic bug label)Something isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedIntake review done: acceptance criteria, links, and normalized labels addedpriority/highImportant and time-sensitiveImportant and time-sensitivequeue/parkedQueue task is parked (waiting on CI, time, or an event)Queue task is parked (waiting on CI, time, or an event)status/parkedDeliberately paused with a recorded resume conditionDeliberately paused with a recorded resume conditionstatus/triagedTriaged and routed (see agent note)Triaged and routed (see agent note)
Description
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/usagewith 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-Afterand 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 < cooldownUntiland a last-good entry exists, emit it marked retained (the existingmarkRetainedmachinery) 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.
Metadata
Metadata
Assignees
Labels
effort/mMedium: expected under 1 day of workMedium: expected under 1 day of workkind/bugSomething isn't working (prefixed form of the classic bug label)Something isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedIntake review done: acceptance criteria, links, and normalized labels addedpriority/highImportant and time-sensitiveImportant and time-sensitivequeue/parkedQueue task is parked (waiting on CI, time, or an event)Queue task is parked (waiting on CI, time, or an event)status/parkedDeliberately paused with a recorded resume conditionDeliberately paused with a recorded resume conditionstatus/triagedTriaged and routed (see agent note)Triaged and routed (see agent note)
Activity
developing-today commentedon Nov 5, 2026
CODE-1605
dezren39 commentedon Nov 5, 2026
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.
dezren39 commentedon Nov 5, 2026
Agent note - handed off
ses_e5e5345bb37ffeVIwaWpY3Yh4Odezren39 commentedon Nov 5, 2026
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.