Skip to content

fix(routing): skip known-exhausted accounts at admission#57

Open
iceteaSA wants to merge 4 commits into
cortexkit:mainfrom
iceteaSA:fix/routing-skip-exhausted
Open

fix(routing): skip known-exhausted accounts at admission#57
iceteaSA wants to merge 4 commits into
cortexkit:mainfrom
iceteaSA:fix/routing-skip-exhausted

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Stacked on #54 and #56 — the base merge (145c489) brings isQuotaExhausted (#54) and the getSidebarState(path) overload + snapshot-level quota.checkedAt (#56). Only the top commit (d5f1fdd) is this PR's change; merge those two first and this reduces to one commit.

Why

Rate-limit marks are per-process and in-memory. With several concurrent opencode processes and fallback-first routing pinned at an exhausted account, every process independently pays one doomed admission probe (admission-time usage_limit_reached → mark → retryable error → reroute) before learning what the machine-global sidebar file already knows: the account is at 100% with a reset days away. The operator sees each discovery as a visible retry/error flash.

What

Admission-time candidate selection now consults quota before probing:

  • Dual source with freshness precedence: in-memory QuotaManager peek vs the shared sidebar file row (compared by primary.checkedAt, then snapshot checkedAt, then entry checkedAt). The fresher source is selected — the file wins only when strictly newer, memory wins ties, and an empty in-memory cache (fresh process) defers to a valid file row. isQuotaExhausted (type-safe, fail-open) is applied only to the selected source.
  • Fallback filter: exhausted candidates are dropped after the existing killswitch/rate-limit filters; skipped accounts are never probed, so their backoff/mark state is untouched. Applied to both the proactive (fallback-first) gate and the reactive iterator via a shared memoized selection.
  • Exhausted primary: synthesizes the existing killswitch-style 429 (reason quota-exhausted, Retry-After from the account's own resetsAt) so the reroute happens without the doomed probe — only when a non-exhausted fallback survives.
  • Safety valves: unknown/missing/malformed/past-reset quota is never exhausted (fail-open); if filtering would remove the last admission path, the current wire-probe order is fully restored — a stale or corrupt file can never brick routing, the wire stays the final authority. Sidebar file read at most once per request, tolerant reader, no-throw.
  • Each skip logs on the quota channel: admission skip: exhausted account {accountId, source, resetsAt}.

Verification

  • RED-first: exhausted first fallback (expected client-alt, got work-alt), fresh-process file exhaustion, newer-exhausted-file vs stale-healthy-memory, exhausted primary reroute — all fail pre-fix; 7 fail-open characterizations proven non-vacuous by reverse-applying the src diff (they pass on reverted source, i.e. they pin non-interference).
  • Review (gemini-3.1-pro): APPROVE 0 must / 0 should — adversarial-file surface (block/steer/brick), empty-cache tie-break, filter ordering (no state mutation for skipped accounts), once-per-request memoization, proactive+reactive coverage, and reverse-apply RED all verified.
  • Gates: build ✓ · tsc ✓ · full suite green ✓ · biome clean.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Skip probing accounts that are already quota‑exhausted at admission by using the freshest of in‑memory and shared sidebar data. Adds a background quota refresh to keep snapshots current, reducing error flashes and wasted retries.

  • New Features

    • Admission quota check selects the fresher source (memory vs getSidebarState) and drops exhausted fallbacks; if main is exhausted, synthesize a 429 with Retry-After from its own resetsAt and reroute when a healthy fallback exists.
    • Background refresher (BackgroundQuotaRefresh, refreshQuotaInBackground) runs ~every 5m with jitter, respects backoff, skips snapshots fresher than 4m, and updates the sidebar only when quota changes.
    • Sidebar/state updates: add checkedAt to quota, isQuotaExhausted, avoid exhausted entries in resolveSessionSidebarRouting, and record routing for a parent session via x-parent-session-id.
  • Bug Fixes

    • Safety valves: fail-open on missing/malformed/past-reset quota; if filtering would remove the last path, keep the original probe order.
    • Prevent stale overwrites: setSidebarMachineState now merges quota by freshness so newer on-disk snapshots aren’t clobbered.

Written for commit d5f1fdd. Summary will update on new commits.

Review in cubic

@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.

4 issues found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/core/refresh-all-quota.ts">

<violation number="1" location="packages/opencode/src/core/refresh-all-quota.ts:91">
P2: A future `checkedAt` suppresses polling until that future time plus the freshness window, so clock-skewed or malformed shared state can leave quotas unrefreshed indefinitely. Treat timestamps later than `now` as stale.</violation>
</file>

<file name="packages/opencode/src/index.ts">

<violation number="1" location="packages/opencode/src/index.ts:162">
P2: Overlapping plugin lifecycles can silently disable quota polling for the active loader: a newer loader replaces this singleton's callback, then an older loader's dispose stops it. Keep the refresh controller per `CodexAuthPlugin`/loader or add ownership before stopping it.</violation>

<violation number="2" location="packages/opencode/src/index.ts:2233">
P1: Re-login can incorrectly hard-block the new main account when the shared sidebar still contains a fresher exhausted quota for the previous account. Persist/compare the main account identity with the sidebar quota, or fail open on the file source when it cannot be tied to `mainAccountIdentity`.</violation>
</file>

<file name="packages/opencode/src/tests/integration.test.ts">

<violation number="1" location="packages/opencode/src/tests/integration.test.ts:3411">
P2: Helper `writeAdmissionSidebarState` writes sidebar state with `route: 'fallback-first'` hardcoded, but the test `admission quota skips a file-exhausted main without probing it` uses `mode: 'main-first'` in `seedAdmissionAccounts` and never overrides route in the sidebar state write. This means sidebar state says `fallback-first` while config says `main-first`, creating a mismatch that may mask whether the routing mode is correctly respected by the admission logic.</violation>
</file>

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

Re-trigger cubic

const sidebarState = await requestSidebarState()
const mainQuotaDecision = admissionQuotaDecision(
quotaManager.peekMainForPolicy(mainAccountIdentity),
sidebarState.main.quota,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Re-login can incorrectly hard-block the new main account when the shared sidebar still contains a fresher exhausted quota for the previous account. Persist/compare the main account identity with the sidebar quota, or fail open on the file source when it cannot be tied to mainAccountIdentity.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/index.ts, line 2233:

<comment>Re-login can incorrectly hard-block the new main account when the shared sidebar still contains a fresher exhausted quota for the previous account. Persist/compare the main account identity with the sidebar quota, or fail open on the file source when it cannot be tied to `mainAccountIdentity`.</comment>

<file context>
@@ -2044,6 +2222,36 @@ export async function CodexAuthPlugin(
+            const sidebarState = await requestSidebarState()
+            const mainQuotaDecision = admissionQuotaDecision(
+              quotaManager.peekMainForPolicy(mainAccountIdentity),
+              sidebarState.main.quota,
+              Date.now(),
+            )
</file context>

(checkedAt) =>
typeof checkedAt === 'number' &&
Number.isFinite(checkedAt) &&
deps.now() - checkedAt < freshnessMs,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A future checkedAt suppresses polling until that future time plus the freshness window, so clock-skewed or malformed shared state can leave quotas unrefreshed indefinitely. Treat timestamps later than now as stale.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/core/refresh-all-quota.ts, line 91:

<comment>A future `checkedAt` suppresses polling until that future time plus the freshness window, so clock-skewed or malformed shared state can leave quotas unrefreshed indefinitely. Treat timestamps later than `now` as stale.</comment>

<file context>
@@ -65,53 +68,86 @@ export async function refreshAllQuota(
+      (checkedAt) =>
+        typeof checkedAt === 'number' &&
+        Number.isFinite(checkedAt) &&
+        deps.now() - checkedAt < freshnessMs,
+    )
 
</file context>
Suggested change
deps.now() - checkedAt < freshnessMs,
checkedAt <= deps.now() && deps.now() - checkedAt < freshnessMs,

const HANDLED_SENTINEL = '__OPENCODE_OPENAI_AUTH_COMMAND_HANDLED__'

let bootQuotaSeedStarted = false
const backgroundQuotaRefresh = new BackgroundQuotaRefresh()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Overlapping plugin lifecycles can silently disable quota polling for the active loader: a newer loader replaces this singleton's callback, then an older loader's dispose stops it. Keep the refresh controller per CodexAuthPlugin/loader or add ownership before stopping it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/index.ts, line 162:

<comment>Overlapping plugin lifecycles can silently disable quota polling for the active loader: a newer loader replaces this singleton's callback, then an older loader's dispose stops it. Keep the refresh controller per `CodexAuthPlugin`/loader or add ownership before stopping it.</comment>

<file context>
@@ -152,6 +159,7 @@ const DEFAULT_MID_STREAM_RATE_LIMIT_RESET_MS = 60_000
 const HANDLED_SENTINEL = '__OPENCODE_OPENAI_AUTH_COMMAND_HANDLED__'
 
 let bootQuotaSeedStarted = false
+const backgroundQuotaRefresh = new BackgroundQuotaRefresh()
 const logModels = createLogger('models')
 let loggedCostRestoration = false
</file context>

@@ -1,5 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, test } from 'bun:test'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Helper writeAdmissionSidebarState writes sidebar state with route: 'fallback-first' hardcoded, but the test admission quota skips a file-exhausted main without probing it uses mode: 'main-first' in seedAdmissionAccounts and never overrides route in the sidebar state write. This means sidebar state says fallback-first while config says main-first, creating a mismatch that may mask whether the routing mode is correctly respected by the admission logic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/tests/integration.test.ts, line 3411:

<comment>Helper `writeAdmissionSidebarState` writes sidebar state with `route: 'fallback-first'` hardcoded, but the test `admission quota skips a file-exhausted main without probing it` uses `mode: 'main-first'` in `seedAdmissionAccounts` and never overrides route in the sidebar state write. This means sidebar state says `fallback-first` while config says `main-first`, creating a mismatch that may mask whether the routing mode is correctly respected by the admission logic.</comment>

<file context>
@@ -3213,6 +3393,357 @@ describe('integration: active fallback routing', () => {
+        now + 3600_000,
+      )
+      hooks = loaded.hooks
+      writeAdmissionSidebarState({
+        fallbackIds: ['work-alt', 'client-alt'],
+        fallbackQuotas: {
</file context>

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