Repository navigation
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.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
919c76e6-672b-4f63-b282-fe88533563da
📒 Files selected for processing (18)
src/main/claude-accounts/claude-account-auto-switch-policy.test.tssrc/main/claude-accounts/claude-account-auto-switch-policy.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/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-pane-claude-section.test.tsxsrc/renderer/src/components/settings/accounts-pane-claude-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.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
…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.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Track the read timestamp only for eligible candidates. · claude-account-auto-switch.ts:133-135
src/main/claude-accounts/claude-account-auto-switch.ts:133-135
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTrack the read timestamp only for eligible candidates.
When a non-candidate inactive account is fetching,
decidecan request a candidate read, butfetchInactiveClaudeAccountsOnOpen()returns because another inactive fetch is already running. The current listener has already setlastUsageReadAt, so the eligible candidate can remain stale until the 15-minute interval expires. Limit the timestamp update to the same candidate set used bydecide.Suggested fix
} selectedAccountId = selectedId // Why: the usage service skips a read it debounces, so only one under way counts as tried. - if (state.inactiveClaudeAccounts.some((usage) => usage.isFetching)) { + const candidateIds = new Set( + deps.accounts.listAccounts().accounts + .filter( + (account) => + account.id !== selectedId && + !account.needsSignIn && + isOnTarget(account, state.claudeTarget) + ) + .map((account) => account.id) + ) + if ( + state.inactiveClaudeAccounts.some( + (usage) => usage.isFetching && candidateIds.has(usage.accountId) + ) + ) { lastUsageReadAt = now }
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
e92b403a-0dbc-4459-9375-e6e4272984d5
📒 Files selected for processing (2)
src/main/claude-accounts/claude-account-auto-switch.test.tssrc/main/claude-accounts/claude-account-auto-switch.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/claude-accounts/claude-account-auto-switch.ts
- src/main/claude-accounts/claude-account-auto-switch.test.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
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.
|
Small update since I opened this. I kept poking at it and pushed a few follow-ups:
@nwparker @Jinwoo-H @brennanb2025 could one of you approve the workflow run when you have a minute? I ran the And if this just isn't something you want in Orca, tell me and I'll close it. No hard feelings. |
|
+1 for getting this in. We run several Claude accounts on a shared box that Orca drives over SSH, and today the rotation lives outside Orca (claude-swap's The opt-in toggle, the 90% threshold, the margin and the cooldown match what has worked for us. I'm not asking for this PR to grow; it's useful as is. The follow-ups that matter most to us are in #20512 (comment). The main one is the Fable weekly window: it's usually the first limit we hit, so rotation should trigger on it and skip accounts whose Fable is spent. Designing the policy so per-model windows can be added later would make that an easy next step. One question: when the accounts are on an SSH host, does this policy run there, or only for accounts on the desktop? |
|
Thanks for this, it's the piece I was missing too. I built two small follow-ups on top of your branch, mostly from the requests in #20512:
The policy is unchanged apart from taking the line as a parameter. Exclusion is one more filter next to Commit: f8team@757471f (one commit on top of One related note, not caused by this PR: on a machine in the #28287 state, System default's login can belong to a different account than its label, so a reading tagged Would you rather fold it into this PR, or have me open it as a separate PR once this one lands? I'm fine either way and don't want to make this one harder to review. |
|
@bacaxnot thanks, good to hear the defaults match what you already run. On the SSH question: it runs wherever the accounts live. On an Fable weekly makes sense as the next step. The check takes the tightest window that's still running, so Fable would be one more window in it, which also keeps it from picking an account whose Fable is spent. If you can try it on your box once it's in a build, that would help a lot. |
|
@f8team nice, thanks for building on it, and for doing the remove-a-guard check too. I'd keep it separate. This one hasn't had a maintainer review yet, and another 500 lines would make it harder to get through. Once it lands, please open yours as its own PR so it's credited to you. I'll review it and link it from here. Good catch on #28287. If that changes how |
|
@bacaxnot the other half of what For your setup: for now it only moves panes running on the desktop's own machine, so sessions on the SSH host are skipped. That's the next thing I'd look at once it's in. |
# Conflicts: # src/main/claude-accounts/claude-account-selection.ts # src/main/claude-accounts/claude-account-service.test.ts # src/main/claude-accounts/service.ts # src/main/startup/main-process-account-services.ts # src/renderer/src/components/settings/accounts-claude-search.ts # src/renderer/src/components/settings/accounts-pane-claude-section.test.tsx # src/renderer/src/components/settings/accounts-pane-claude-section.tsx # src/renderer/src/i18n/locales/en.json # src/shared/managed-account-types.ts
…ch as after one by hand The automatic switch runs in main, so the running-session question from stablyai#29898 never came up for it. Main now tells the renderer which accounts it switched between, and the renderer hands that to the same step a switch from the menu runs: ask, always restart, or never restart.
…unt's own distro With the default WSL distro the usage target names no distro, so the running-session step looked in the wrong lane and missed every session in the account's distro. A switch from the menu passes the concrete distro; the automatic one now does too.
…following it waits on nothing Following the switch read the account list first, so a switch by hand during that read was overtaken: the follow then asked about the automatic one, and a failed read was left unhandled. Main has the list at the switch and now sends it along.
|
Merged main in. #29898 and #29209 changed the same files, so a few things moved:
Most conflicts were both sides adding things next to each other. The Claude section now has both your running-session setting and the auto-switch toggle. |
ELI5
If you keep more than one Claude account in Orca, the annoying moment is when the one you're on hits its 5-hour or weekly limit. Every new agent just stops until you notice and switch by hand. This adds an opt-in setting so Orca switches for you a bit before that.
What Changed
Before: the selected account runs out, and new Claude sessions stop at the limit until you open the picker and choose another one.
After: Settings › Accounts › Claude gets a toggle, Switch accounts before a usage limit. It's off by default and only shows up with two or more accounts on this device. When it's on and the selected account goes past 90% of its 5-hour or weekly window, Orca selects the account with the most room left and shows a notification. Same as switching by hand, it applies to the next Claude you start, and running Claude sessions get the same treatment as after a switch by hand: #29898's When you switch accounts while Claude is running setting decides (ask, always restart, never restart).
How it works, briefly:
RateLimitService.onStateChangelooks at usage Orca already has. The decision itself is a small pure function inclaude-account-auto-switch-policy.ts.usageMetadata.authProvenance) and whose window hasn't already reset. Otherwise a session still running on the old account, or an idle one re-posting old numbers (fix(rate-limits): ignore stale Claude statusline snapshots from idle sessions #23421), could cause a wrong switch.fetchInactiveClaudeAccountsOnOpen, at most every 15 minutes, so it doesn't hit the usage endpoint more often than Orca's own poll.ClaudeAccountSelection.selectthe picker uses. If it has to wait behind a sign-in, it checks again when its turn comes and drops out if you did anything in the meantime.Two changes sit outside the accounts folder. A successful usage read now carries the account tag that failed reads and live session posts already had (
claude-active-usage-fetch.ts). And the Claude settings-search entries moved toaccounts-claude-search.ts, becauseaccounts-search.tswas at the 300 line cap. There's also one paragraph in the docs.Why
Orca already holds the credentials and the usage numbers for every managed account, so it's the safe place to do this. Today people use claude-swap for it, which writes
~/.claudeand the Keychain directly and ends up fighting Orca's managed auth. #20512 has a good table on that.I went with a threshold instead of reacting to a 429 because by the time you get a 429 the work has already stopped. The defaults (90%, 5 minutes, 10 points) are claude-swap's.
Not in here, on purpose: Codex and the CLI (#8770 / #26870). Moving sessions that are already running is #29898's; this only hands the automatic switch to it. The policy doesn't know it's Claude, so Codex could reuse it later.
Linked Issue
Fixes #20512 (the Claude side)
Visual Proof
Settings › Accounts › Claude with two accounts, from the real app (headless e2e launch,
mainvs this branch).Testing
Ran locally on macOS, following
pr.yml: the account, rate-limit, settings and status-bar suites, typecheck (node, web, cli), oxlint, the changed-code and type-aware audits, the ratchets and the localization checks.Red for me, but not from this diff:
check:main-sync-spawn-ratchetcrashed onmainwhen I ran it ([Bug]: the main-process sync-launch ratchet check crashes on main (stale imports after #27788) #28052). fix(ci): point the main sync-spawn ratchet at the shared ratchet library #28054 has fixed it since.codex-fetcher-process-contract.test.tsand a fewsrc/sharedandsrc/main/startupsuites fail the same way on untouchedmain. I'm on Node 26 rather than the pinned 24, which is probably why.Review
cc @nwparker @Jinwoo-H @brennanb2025, you've been in this code the most lately. Two things I'm unsure about: whether the toggle belongs in this spot, and whether 90% is the right default.
Agent skill upstream boundary
Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
coveredBySystemDefault), which older clients ignore.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)