Skip to content

Quota: error recovery, probe-on-add, and usage-endpoint quota - #16

Closed
brendandebeasi wants to merge 17 commits into
KarpelesLab:masterfrom
brendandebeasi:pr/quota-recovery-usage
Closed

brendandebeasi wants to merge 17 commits into
KarpelesLab:masterfrom
brendandebeasi:pr/quota-recovery-usage

Conversation

@brendandebeasi

Copy link
Copy Markdown

Stack position: 3 of 4. Builds on #14 and #15 (merge those first). The diff shown here includes their commits until they merge; the changes unique to this PR are below.

What this adds (net-new vs #15)

  • Error-state recovery — accounts parked in the error state are re-evaluated and recovered automatically instead of staying dead until restart (account-manager.js, prober.js).
  • Probe quota on add — when an account is added it's probed immediately, and status shows quota freshness, so a newly added account routes correctly from the first request (account-manager.js, index.js).
  • Usage-endpoint quota — read quota from Anthropic's OAuth usage endpoint, which is cold (no message spend), so quota can be known without burning tokens on a probe (account-manager.js, oauth.js, prober.js, index.js).

Why

Makes quota tracking accurate and self-healing: accounts recover on their own, new accounts are usable immediately, and quota can be read without cost.

Test plan

  • An account forced into error recovers without restart.
  • Newly added account shows quota immediately and routes correctly.
  • Usage-endpoint path populates quota with no message spend.

Brendan DeBeasi and others added 17 commits May 5, 2026 21:24
    Extracts the `formatReset` helper into `src/index.js` and updates the
    `teamclaude status` command to display time-to-reset (e.g., "resets in 2h30m")
    next to the usage percentages for both unified and standard quotas.
Add a computed `remaining` summary to AccountManager.getStatus() and surface it
in `teamclaude status`: session/weekly % left for Claude Max accounts, absolute
tokens/requests left (with %) for API-key accounts, instead of used %.
- --headless flag runs the server without the interactive TUI (for systemd /
  backgrounding)
- POST /teamclaude/reload re-syncs accounts from config on the running server;
  login/import/remove now ping it so changes apply with no restart; SIGHUP does
  the same (systemd ExecReload)
- syncAccountsFromDisk now removes accounts deleted from config too (was add-only),
  keeping the in-memory config and AccountManager in lockstep
Quota utilization + reset times are only returned in /v1/messages response
headers, so idle accounts show no quota until they're rotated to. Add a prober
that periodically (config.pokeIntervalMs, default 10m; 0=off) sends a minimal
1-token request to each stale/idle account and feeds the rate-limit headers into
updateQuota (without counting it as real usage). For Claude Max (OAuth) accounts
the probe must mimic Claude Code, so the prober learns the request shape (beta
header + system prompt + model) from successful live traffic and replays it.
Each probe is bounded by a 20s timeout.
Add a computed `remaining` summary to AccountManager.getStatus() and surface it
in `teamclaude status`: session/weekly % left for Claude Max accounts, absolute
tokens/requests left (with %) for API-key accounts, instead of used %.
Headless mode, reload API, and atomic account add/remove
Background quota prober for idle accounts
The reload diff in syncAccountsFromDisk decided removals with an existence
check, so when two config entries shared a name (e.g. one email across
multiple orgs) removing one left the survivor's name satisfying the check for
the removed in-memory account — the running server never dropped it and it
lingered in `teamclaude status`.

Reconcile by count instead: each disk entry claims at most one in-memory
account (UUID match preferred, then name); anything left unclaimed is removed.
…ware

Fix account removal not propagating on reload for same-named accounts
The same email/person can belong to multiple Anthropic orgs, each with its own
OAuth token and quota. Identity was keyed on accountUuid (the person) + name
(the email), so a second org overwrote the first on login, dedup collapsed
them, and rotated-token persistence/removals matched the wrong entry.

Introduce src/identity.js with an org-aware sameIdentity(): same accountUuid
requires matching org (orgUuid, falling back to orgName), while a still-unknown
org on either side is treated as the same account so a profiled login backfills
a legacy entry instead of duplicating it. Capture organization.uuid in
fetchProfile, carry orgUuid/orgName on accounts (and in status), and route every
match site (upsert dedup, findConfigAccount token persistence, reload sync
pairing, accounts dedup, TUI import) through sameIdentity. Display names stay the
plain email until a second org collides, then both become "email (orgName)".
Support multiple orgs per email (org-aware identity)
A stale/rotated OAuth token left an account stuck in status 'error' with
nothing to retry it: the prober skipped errored accounts and a successful
refresh never cleared the flag. Now ensureTokenFresh flips 'error'→'active' on a
successful refresh, and the prober force-refreshes errored OAuth accounts each
tick (clearing the error when the refresh succeeds, leaving it for the next tick
when the refresh token is genuinely dead). The underlying cross-contamination
cause was addressed by the org-aware token persistence in the prior change.
Recover accounts from the error state
A freshly added/logged-in account had no quota data until the next poke
interval (up to 10 min), and status gave no hint of when quota was last seen.
Now a reload that adds accounts kicks an immediate prober pass, and status
reports each account's quota freshness ("updated 12s ago" / "no data yet")
plus its org. API-key accounts populate immediately; OAuth still waits for a
request template (existing prober constraint), but the freshness line makes
that state visible.
Probe quota on account add + show quota freshness in status
The prober could only learn OAuth quota by replaying a Claude Code-shaped
/v1/messages request, which required first learning a template from live
traffic — so a freshly added account showed no quota until real traffic flowed.

Use Anthropic's GET /api/oauth/usage endpoint (anthropic-beta:
oauth-2025-04-20) instead: it returns five_hour/seven_day utilization + reset
directly, works with no template and no message spend, and populates quota the
moment an account is added (probe-on-add now actually fills it in). API-key
accounts keep the /v1/messages header probe. login/import also print the new
account's current quota immediately so you can confirm it works.
Read OAuth quota from /api/oauth/usage (cold, no spend)
MagicalTux added a commit that referenced this pull request Jun 18, 2026
One Anthropic account (email/accountUuid) can hold multiple subscriptions
across different organizations — e.g. a corporate Pro org and a personal Max
org. The old identity model keyed only on accountUuid (with name as fallback),
so adding the second org overwrote the first (#6) and --name couldn't help
because the UUID check won (#7).

Identity is now (accountUuid + organization). Re-implemented and reviewed
against master rather than cherry-picking the closed fork PRs (#15/#17).

- src/identity.js (new): orgKey / sameIdentity / matchAccounts / emailOf, the
  single source of truth for identity and lookups. sameIdentity treats an
  unknown org on either side as a match so a freshly-profiled login backfills a
  legacy entry instead of duplicating it.
- fetchProfile: extract organization.uuid; accounts persist orgUuid/orgName.
- All dedup/sync/find match sites switched to sameIdentity; syncAccountsFromDisk
  uses a greedy 1:1 claim so multiple same-person/different-org entries pair
  correctly. accounts command dedups by (accountUuid, org) and backfills org.
- Name disambiguation: one org -> "email"; multiple -> "email (Org)", renaming
  the existing collider too. Names stay unique (they are the user-facing key).
- Org-aware remove + api: resolve by name or email, --org <name|uuid> to
  disambiguate; lists candidates when ambiguous.
- Per-account rotation priority: `priority` field (lower = preferred, default 0)
  as the primary sort key in _selectNext with the existing weekly heuristic as
  tiebreaker; new `teamclaude priority` command (--first/--last); session-reset
  switching won't demote to a worse priority.
- Tests: identity/matchAccounts/emailOf and priority rotation (22 passing).
- Docs: README, config.example.json, help text.

Out of scope (future PRs from the #18 stack): #14 headless/reload API/prober,
#16 usage-endpoint quota. #13 (429 account-failover) intentionally not taken —
the throttle is IP-based, so the merged #25 back-off is the correct behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MagicalTux added a commit that referenced this pull request Jun 18, 2026
Completes the passive-compatible parts of the #18 upstreaming stack (#14),
without the active background prober (which would violate the passive-only
design).

- `teamclaude server --headless` (alias `--no-tui`) runs the proxy without the
  interactive TUI, for backgrounding / running as a service.
- `POST /teamclaude/reload` re-syncs accounts from config without a restart —
  the headless equivalent of pressing 'R' in the TUI. Local-only control
  endpoint (no upstream calls); returns the number of newly added accounts, 501
  if unwired, 500 on handler error.
- CLI commands that change accounts (login/import/enable/disable/priority) now
  best-effort notify a running server to reload, so changes apply live. A closed
  port refuses instantly, so it's a no-op when nothing is running. (Reload picks
  up adds + credential/priority/enable-disable changes; removals still need a
  restart.)
- reloadAccounts is shared by the TUI 'R' key, the endpoint, and CLI notify.

Explicitly NOT included (active polling, against passive-only): the background
quota prober, usage-endpoint quota, and probe-on-add from #14/#16.

Tests for the reload endpoint (success/501/500); README + help updated.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brendandebeasi
brendandebeasi deleted the pr/quota-recovery-usage branch August 22, 2026 15:31
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