Skip to content

feat(quota): background poll keeps idle account quota fresh#56

Open
iceteaSA wants to merge 1 commit into
cortexkit:mainfrom
iceteaSA:feat/quota-background-poll
Open

feat(quota): background poll keeps idle account quota fresh#56
iceteaSA wants to merge 1 commit into
cortexkit:mainfrom
iceteaSA:feat/quota-background-poll

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Why

Quota is push-primary (x-codex-* headers / WS codex.rate_limits frames), so a fallback account that is not currently being routed to shows stale or missing quota until the operator manually runs /openai-quota. The sibling anthropic-auth plugin keeps idle accounts fresh with a background poll; this brings the same behavior here — without request spam from multiple concurrent opencode processes, and without letting any process publish stale data over fresh.

What

Background poll

  • core/background-quota-refresh.ts — singleton 5-minute timer (±30 s jitter per process), unref'd so it never keeps the process alive, idempotent start, stopped from both the plugin-level and auth-loader dispose paths.
  • Overlap-safe: the timer holds the in-flight run's promise and drops a tick that fires while the previous run is still executing (skip, not queue) — no concurrent token refreshes or duplicate wham calls from a slow tick.
  • Each tick calls the existing refreshAllQuota with respectBackoff: true plus a 4-minute freshness gate (skipFresherThanMs). Rides the 15 s abort timeout + quota-channel logging from fix(quota): bound and log the active quota fetch path #52. Deliberate deviation from the sibling: no immediate first run — the boot seed covers startup; first poll fires at the jittered 5-minute mark.

Cross-process dedup (no locks)

  • The freshness gate consults BOTH the in-memory QuotaManager snapshot AND the machine-global sidebar state file (written on every quota push by any process, read once per tick, tolerant reader, fail-open on missing/corrupt/malformed checkedAt). Whichever process polls first refreshes the file; every other process's tick inside the window sees the fresh checkedAt and skips. Worst case under jitter collision is 2 concurrent fetches, never N.
  • An all-skipped background tick performs no sidebar write (nothing new to publish).

Stale-write clobber fix (machine-state sidebar writes)

  • Previously any process's sidebar write replaced the machine-global quota rows wholesale from its own memory — an idle process with a stale cache could overwrite fresher rows another process just wrote (and thereby corrupt the dedup gate reading those rows). setSidebarMachineState now freshness-merges per-account inside the existing compare-retry callback: for main and each fallback (matched by id), the side with the fresher finite checkedAt wins; invalid/missing loses to valid; both missing → incoming wins. activeId/route keep last-writer semantics; activeRouting merge unchanged.
  • Snapshot-level quota.checkedAt added: a complete authoritative frame may retire every window (no primary.checkedAt left), and without a timestamp on the snapshot itself the freshness merge would resurrect the retired window from disk.

Unchanged: the manual /openai-quota command path (no gate, always writes) and the boot seed. No new config keys, commands, or TUI changes.

Verification

  • RED-first throughout: freshness gate (expected 1 wham call, got 3), cross-process skip (empty memory + fresh file: expected 0 calls, got 3), stale-write clobber (fresh disk rows expected retained, got stale 90/40), all-skipped no-write (expected 0 writes, got 1), tick overlap (expected 1 run, got 2), window-retirement tombstone.
  • Review (gemini-3.1-pro): APPROVE 0 must / 1 should — lifecycle, gate ordering (skip before token refresh), backoff interaction, restart-with-empty-cache behavior, command/boot-seed invariance verified; the should (jitter bounds untested) folded as a test pinning random()=0/1 → ∓30 s.
  • cubic P2 (tick overlap) fixed in the same commit; thread resolved.
  • Gates: build ✓ · tsc ✓ · full suite 692/0 ✓ · biome clean.

@iceteaSA
iceteaSA force-pushed the feat/quota-background-poll branch from 1399ceb to e67f58c Compare July 21, 2026 20:31

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/index.ts
@ualtinok

Copy link
Copy Markdown
Contributor

Thanks. Background quota refresh is useful, but this needs revision before merge:

  1. Rebase onto current main (v0.4.2), which includes bounded quota/OAuth fetches from fix(quota): bound and log the active quota fetch path #52.
  2. Replace sidebar freshness check-then-fetch with an atomic cross-process refresh claim. The sidebar write lock serializes writes after fetching and does not prevent N stale processes from issuing N quota calls.
  3. Bind shared freshness to the current main-account identity and fallback token fingerprint. Quota from a prior login must not suppress a refresh after account switch or re-login.
  4. Route expired main-token refresh through the existing main refresh lease rather than calling the provider directly.
  5. Make lifecycle instance-safe: disposing one loader must not stop another loader's poller, and stop must define behavior for an in-flight run.
  6. Include secondary-window timestamps in freshness selection, and add multi-process, account-switch, re-login, secondary-only timestamp, and disposal tests.

Please rerun the complete gate after these changes.

@iceteaSA
iceteaSA force-pushed the feat/quota-background-poll branch from 823e46a to 3751617 Compare July 22, 2026 18:38
@ualtinok

Copy link
Copy Markdown
Contributor

Thanks for the revision. Rebase and main-refresh lease use are now correct.

Remaining blockers:

  1. Freshness must use live main identity and fallback token identity. Loader-captured main IDs and internal fallback IDs can reuse a prior login's fresh cache. Add stale in-memory re-login coverage, not only sidebar account-switch coverage.
  2. The cross-process refresh lock has a 30-second non-renewing TTL, but serial account refresh can exceed it. Renew the lock or use a bounded claim whose lifetime covers the entire run, with an over-TTL concurrency test.
  3. The poller remains process-global. One loader overwrites another callback and either disposal stops the shared poller. Make it per-loader or reference-counted, and add two-loader disposal plus in-flight-stop tests.
  4. Sidebar freshness merge must consider secondary-window checkedAt, not only primary or snapshot-level timestamps. Add a newer-secondary ordering regression.

Please rerun the full gate after these changes.

@iceteaSA
iceteaSA force-pushed the feat/quota-background-poll branch from 3751617 to 578efd0 Compare July 22, 2026 20:11
@ualtinok

Copy link
Copy Markdown
Contributor

Thanks for the revision. The per-loader lifecycle and in-flight stop work are now covered.

Three blockers remain:

  1. Background main quota writes still store no account identity, and peekMainForPolicy accepts an unknown identity. A background result for the old account can therefore remain fresh after re-login. Fallback in-memory freshness is likewise keyed only by internal ID. Persist and require live identity metadata for both cache paths.
  2. The cross-process background claim still has no renewal. A serial refresh can outlive its TTL and allow a second process to begin the same work. Use the lock's renewal support and add an over-TTL contention regression.
  3. Sidebar quota freshness is account-level, not per-window. A snapshot with a newer primary can replace another snapshot's newer secondary. Merge primary and secondary windows independently and add the crossed-timestamp regression.

Please rerun the full gate after these fixes.

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.

2 participants