Repository navigation
feat(codex-accounts): switch accounts before a usage limit - #28433
ogarciarevett wants to merge 9 commits into
Conversation
Opt-in setting. When the selected managed Claude account passes 90% of its 5-hour or weekly window, new Claude sessions move to the managed account on the same runtime with the most room left. A 10-point margin and a 5-minute cooldown (also after a manual pick) stop two accounts trading places; at a hard limit any account with room wins. Accounts that need a sign-in, sit on another runtime, or have no reading from the last 15 minutes are skipped; stale readings are refreshed before ranking. Only readings tagged with the selected account count, and a switch queued behind a sign-in re-decides on the latest usage and yields to anything the user did meanwhile.
…der way The usage service skips a read it debounces, one already running, or one where every account is inside Retry-After. The auto-switch listener recorded the ask as a read anyway and would not ask again for 15 minutes. It now counts a read only when the published state shows one in flight.
An idle Claude session keeps reporting the usage of a window that has since rolled over (the stale statusline snapshots stablyai#23421 describes), and an inactive account's cached reading can outlive its window. The auto-switch policy counted both as current, so a stale 96% could trigger a switch and an account whose window had reset could be skipped as exhausted. A window past its reset time no longer counts.
A failed read and a live session's statusline post both carry the account they belong to (usageMetadata.authProvenance), but a successful OAuth read carried no metadata at all. The auto-switch only trusts a reading tagged with the selected account, so it never acted on a polled reading: with no session posting, as when the account has already hit its limit, it stayed put. A successful read now carries the same credential source and account tag.
…cted account's runtime Two cases left the auto-switch with nothing to pick. With the default WSL distro the usage target names no distro while accounts do, so every other account was filtered out; candidates are now the accounts on the selected account's own runtime. And an account inside its Retry-After cannot be read, so waiting for its reading stalled ranking while a usable account was right there; it no longer waits for those. The docs paragraph now says the switch needs another account with clearly more room.
…ublish The auto-switch listener asked the usage service for a read from inside the service's own publish loop. The service publishes again from within that call, so the window and paired clients were handed the newer state first and the older one last. The ask now runs after the publish it reacted to. A read also counts as tried only when one of the accounts being ranked is fetching, not an account on another runtime.
📝 Walkthrough
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
🔇 Additional comments: 40
src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts: 1
8-11: LGTM!src/shared/rate-limit-types.ts: 1
70-71: LGTM!src/main/rate-limits/service/service-types.ts: 1
37-43: LGTM!src/main/accounts/account-auto-switch-policy.ts: 1
1-62: LGTM!src/main/claude-accounts/claude-account-auto-switch-policy.test.ts: 1
1-128: LGTM!src/main/codex-accounts/codex-account-auto-switch-policy.test.ts: 1
1-128: LGTM!src/main/codex-accounts/codex-account-usage-provenance.ts: 1
6-29: LGTM!src/main/codex-accounts/codex-account-usage-provenance.test.ts: 1
1-53: LGTM!src/main/rate-limits/service/service-fetch-targets.ts: 1
50-62: LGTM!Also applies to: 96-105, 184-187
src/main/rate-limits/service/service-full-cycle-application.ts: 1
24-24: LGTM!Also applies to: 178-181
src/main/rate-limits/service/service-full-cycle-preparation.ts: 1
34-34: LGTM!Also applies to: 296-296
src/main/rate-limits/service/service-provider-cycles.ts: 1
77-82: LGTM!src/main/rate-limits/claude-active-usage-fetch.ts: 1
62-64: LGTM!src/main/rate-limits/claude-profile-usage.test.ts: 1
103-114: LGTM!src/main/claude-accounts/claude-account-selection.ts: 1
120-120: LGTM!src/main/claude-accounts/claude-account-service.test.ts: 1
132-134: LGTM!Also applies to: 378-401
src/main/rate-limits/service-account-target-selection.test.ts: 1
102-120: LGTM!src/shared/managed-account-types.ts: 1
100-101: LGTM!src/main/claude-accounts/service.ts: 1
22-22: LGTM!Also applies to: 103-119, 156-159
src/main/codex-accounts/service.ts: 1
81-82: LGTM!Also applies to: 203-203, 248-263
src/main/codex-accounts/service-account-selection-and-removal.test.ts: 1
144-230: LGTM!src/main/claude-accounts/claude-account-auto-switch.test.ts: 1
1-367: LGTM!src/main/codex-accounts/codex-account-auto-switch.ts: 1
1-177: LGTM!src/main/codex-accounts/codex-account-auto-switch.test.ts: 1
1-399: LGTM!src/main/startup/main-process-account-services.ts: 1
7-11: LGTM!Also applies to: 87-146, 164-169
src/shared/global-settings-types.ts: 2
323-324: LGTM!
312-313: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.Move the new field below the orphaned
Why:comment, or move that comment.Line 311 is a
Why:doc comment about preparing shared~/.codex. The newcodexAutoSwitchEnableddoc comment now sits between that comment andcodexManagedAccounts. As a result, TypeScript attaches both comments tocodexAutoSwitchEnabled. Move the new lines above line 311.src/shared/default-global-settings.ts: 1
184-185: LGTM!src/renderer/src/components/settings/accounts-pane-claude-section.tsx: 1
8-8: LGTM!Also applies to: 368-399
src/renderer/src/components/settings/accounts-pane-claude-section.test.tsx: 1
157-169: LGTM!src/renderer/src/components/settings/accounts-pane-codex-section.tsx: 1
7-7: LGTM!Also applies to: 18-19, 289-319
src/renderer/src/components/settings/accounts-codex-search.ts: 1
1-76: LGTM!src/renderer/src/components/settings/accounts-pane-codex-auto-switch.test.tsx: 1
1-136: LGTM!src/renderer/src/components/settings/accounts-search.test.ts: 1
18-21: LGTM!Also applies to: 45-53, 95-103
src/renderer/src/components/settings/accounts-search.ts: 1
5-8: LGTM!src/renderer/src/components/settings/AccountsPane.tsx: 1
30-30: LGTM!src/renderer/src/components/settings/accounts-claude-search.ts: 1
1-46: LGTM!src/renderer/src/i18n/locales/en.json: 1
19612-19618: LGTM!Also applies to: 19623-19629
docs/site/content/docs/agents/claude-code.mdx: 1
25-26: LGTM!docs/site/content/docs/agents/codex.mdx: 1
21-22: LGTM!
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
e861a2a9-5637-4dcd-99e2-39955ba3f75a
📒 Files selected for processing: 40
docs/site/content/docs/agents/claude-code.mdxdocs/site/content/docs/agents/codex.mdxsrc/main/accounts/account-auto-switch-policy.tssrc/main/claude-accounts/claude-account-auto-switch-policy.test.tssrc/main/claude-accounts/claude-account-auto-switch.test.tssrc/main/claude-accounts/claude-account-auto-switch.tssrc/main/claude-accounts/claude-account-selection.tssrc/main/claude-accounts/claude-account-service.test.tssrc/main/claude-accounts/service.tssrc/main/codex-accounts/codex-account-auto-switch-policy.test.tssrc/main/codex-accounts/codex-account-auto-switch.test.tssrc/main/codex-accounts/codex-account-auto-switch.tssrc/main/codex-accounts/codex-account-usage-provenance.test.tssrc/main/codex-accounts/codex-account-usage-provenance.tssrc/main/codex-accounts/service-account-selection-and-removal.test.tssrc/main/codex-accounts/service.tssrc/main/ipc/pty-runtime-hidden-at-spawn-mark.test.tssrc/main/rate-limits/claude-active-usage-fetch.tssrc/main/rate-limits/claude-profile-usage.test.tssrc/main/rate-limits/service-account-target-selection.test.tssrc/main/rate-limits/service/service-fetch-targets.tssrc/main/rate-limits/service/service-full-cycle-application.tssrc/main/rate-limits/service/service-full-cycle-preparation.tssrc/main/rate-limits/service/service-provider-cycles.tssrc/main/rate-limits/service/service-types.tssrc/main/startup/main-process-account-services.tssrc/renderer/src/components/settings/AccountsPane.tsxsrc/renderer/src/components/settings/accounts-claude-search.tssrc/renderer/src/components/settings/accounts-codex-search.tssrc/renderer/src/components/settings/accounts-pane-claude-section.test.tsxsrc/renderer/src/components/settings/accounts-pane-claude-section.tsxsrc/renderer/src/components/settings/accounts-pane-codex-auto-switch.test.tsxsrc/renderer/src/components/settings/accounts-pane-codex-section.tsxsrc/renderer/src/components/settings/accounts-search.test.tssrc/renderer/src/components/settings/accounts-search.tssrc/renderer/src/i18n/locales/en.jsonsrc/shared/default-global-settings.tssrc/shared/global-settings-types.tssrc/shared/managed-account-types.tssrc/shared/rate-limit-types.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.
ELI5
With this option enabled, Orca chooses a Codex account with more usage left before the selected account runs out. The next Codex session uses that account; sessions already running keep their account. The option is off by default and appears when there are at least two managed Codex accounts.
What Changed
Why
This adds the Codex half missing from #28044, which implements Claude auto-switching. Sharing its provider-neutral policy keeps the thresholds consistent without rewriting the Claude service. Codex retains its own account store and normal selection path, so this needs no credential copying, running-session migration, or new account-selection command.
Linked Issue
Fixes #20512 (the Codex half; the Claude half is #28044, which this PR builds on).
Visual Proof
Settings, AI Provider Accounts, Codex, in an isolated desktop profile with two synthetic managed Codex accounts (
WorkandPersonal, no real credentials).The switch notification is a native macOS notification, outside the renderer capture, so it is not shown; its text is covered by the service tests.
Testing
Tests were written first and observed failing before implementation. The focused run passes 117 tests in 9 suites, including the existing Claude policy and service suites. Validation on macOS with Node 24.21.0 and Bun 1.4.2:
pnpm typecheck: passed, including the final build’s typecheck.pnpm run check:code-quality:changed pr-28044-claude-autoswitch: passed with zero new findings, including design-system checks.pnpm test: 115,840 passed and 50 failed across 21 files. Replaying those files on unchanged feat(claude-accounts): switch accounts before a usage limit #28044 reproduced 36 of the failed cases and the relay-region suite load error. The remaining IPC hidden-spawn failure was traced to an uncached real login-shell probe exceeding its one-second wait; a test-local resolver mock now passes all 9 cases, including with a deliberately slow synthetic shell.pnpm lint: stops on four missing named exports in the untouched main synchronous-spawn ratchet script. Reproduced on unchanged feat(claude-accounts): switch accounts before a usage limit #28044.pnpm build: typecheck, CLI, desktop, web, and mobile-web build and verify successfully. macOS native computer-helper packaging fails becauselipocannot find the expected Swift output paths; reproduced withpnpm run build:nativeon unchanged feat(claude-accounts): switch accounts before a usage limit #28044.The fresh worktree also needed
pnpm --dir mobile install --frozen-lockfile, because mobile is a separate workspace. No dependency manifests or lockfiles changed.Re-checked on a second Mac (macOS 26.4.1, arm64, Node 24.13.1) at head ae05e72:
pnpm run typecheckpassed; the 9 focused suites passed (117 tests, 0 failed);pnpm run lintreports 9 named-import warnings, all reproduced without this change (5 on upstream main, 4 on the #28044 base). The macOS native helper packaging failure above is tracked in #28430 with a separate one-line fix.AI Disclosure
GPT-6.1-Sol (Codex) helped investigate and write this change and its tests. Gemini 3.8 Flash and Claude Opus 5.5 reviewed it.
Review
Agent skill upstream boundary
Notes
This branch is stacked on #28044 at bdbdda4, so the diff against main includes #28044's commits. Please review the commits after bdbdda4; I will rebase onto main when #28044 merges.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred): typecheck passes; full lint, suite and build failures are reproduced on the base, see Testing