Skip to content

feat(claude-accounts): switch accounts before a usage limit - #28044

Open
adifsgaid wants to merge 11 commits into
stablyai:mainfrom
adifsgaid:claude-auto-switch
Open

adifsgaid wants to merge 11 commits into
stablyai:mainfrom
adifsgaid:claude-auto-switch

Conversation

@adifsgaid

@adifsgaid adifsgaid commented Oct 12, 2026 •

Copy link
Copy Markdown

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:

  • A listener on RateLimitService.onStateChange looks at usage Orca already has. The decision itself is a small pure function in claude-account-auto-switch-policy.ts.
  • The target has to be under 90% and at least 10 points better, and there's a 5 minute cooldown after any account change (yours included), so two accounts near the line don't ping-pong. At 100% those rules relax.
  • It only acts on a reading that belongs to the selected account (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.
  • Other accounts' usage is refreshed through the existing fetchInactiveClaudeAccountsOnOpen, at most every 15 minutes, so it doesn't hit the usage endpoint more often than Orca's own poll.
  • The switch goes through the same ClaudeAccountSelection.select the 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 to accounts-claude-search.ts, because accounts-search.ts was 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 ~/.claude and 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, main vs this branch).

Before After
before-light.png after-light.png
before-dark.png after-dark.png

Testing

  • Automated tests added. The policy has its own test file. The listener test covers the nasty cases: the previous account's usage arriving mid-switch, a switch queued behind a sign-in, a usage read that was skipped or is inside Retry-After, WSL, and a window that already reset. For each guard I removed it and checked that a test went red.
  • I manually tested these changes locally. Honest answer: only the settings UI (screenshots above). I haven't watched a real switch happen with two paid subscriptions.

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:

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

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

  • Security: no new credential handling, it reuses the existing select path.
  • Compatibility: the setting is optional and off by default. There is one new optional field on the Claude account summary (coveredBySystemDefault), which older clients ignore.
  • Performance: the listener returns immediately unless the setting is on and the selected account is over 90%.
  • Remote: when Settings shows accounts owned by a remote server, the toggle is hidden, since that host's own settings decide.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

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

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 63bd2a96-c467-4b18-b0ec-f7f43f6afa5b
📥 Commits

Reviewing files that changed from the base of the PR and between 55ceeae and bdbdda4.

📒 Files selected for processing (4)
  • src/main/claude-accounts/claude-account-auto-switch-policy.test.ts
  • src/main/claude-accounts/claude-account-auto-switch-policy.ts
  • src/main/claude-accounts/claude-account-auto-switch.test.ts
  • src/main/claude-accounts/claude-account-auto-switch.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/claude-accounts/claude-account-auto-switch-policy.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

📝 Walkthrough

Adds an opt-in, default-off setting for Claude account auto-switching. The policy evaluates session and weekly usage and selects an eligible account when its thresholds are met. The rate-limit handler checks reading provenance and freshness, requests inactive-account readings, and rechecks queued selections. Successful switches can trigger desktop notifications. The change also adds settings search entries, System-default coverage data, and tests for policy decisions and switching behavior.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to bdbdd

No actionable merge-blocking issue is established; the change is ready for normal validation.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Linked Issues check Warning Direct issue #20512 remains broader than this PR. The PR adds and tests opt-in Claude switching for new sessions, but it does not implement the issue's Codex coverage, configurable threshold, running-… Implement and test the remaining coding requirements in #20512, or link this Claude-only implementation to a separate Claude-scoped issue and remove #20512 as the direct target.
Docstring Coverage Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 19 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Out of Scope Changes check Passed The auto-switch policy, usage provenance, account selection, settings control, search entries, notifications, documentation, and tests all support the Claude account auto-switch feature. The account-s…
Title check Passed The title clearly and concisely describes the primary change: automatic Claude account switching before a usage limit.
Description check Passed The description is complete and relevant. It includes the ELI5 summary, user-facing behavior, implementation details, rationale, linked issue, visual proof, testing results, compatibility notes, and c…
Full details: Out of Scope Changes check

Explanation

The auto-switch policy, usage provenance, account selection, settings control, search entries, notifications, documentation, and tests all support the Claude account auto-switch feature. The account-summary field supports correct System default behavior. No unrelated product-area change is demonstrated.

Full details: Description check

Explanation

The description is complete and relevant. It includes the ELI5 summary, user-facing behavior, implementation details, rationale, linked issue, visual proof, testing results, compatibility notes, and checklist status. It also documents known unrelated test failures and the scope exclusions.

Full details: Linked Issues check

Explanation

Direct issue #20512 remains broader than this PR. The PR adds and tests opt-in Claude switching for new sessions, but it does not implement the issue's Codex coverage, configurable threshold, running-session failover, headless or CLI parity, explicit account pinning, reset-time circuit breaker, or earliest-reset handling when all accounts are exhausted. The current PR description confirms these omissions.

Resolution

Implement and test the remaining coding requirements in #20512, or link this Claude-only implementation to a separate Claude-scoped issue and remove #20512 as the direct target.

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 919c76e6-672b-4f63-b282-fe88533563da
📥 Commits

Reviewing files that changed from the base of the PR and between c62c5a4 and 3dfbfc1.

📒 Files selected for processing (18)
  • src/main/claude-accounts/claude-account-auto-switch-policy.test.ts
  • src/main/claude-accounts/claude-account-auto-switch-policy.ts
  • src/main/claude-accounts/claude-account-auto-switch.test.ts
  • src/main/claude-accounts/claude-account-auto-switch.ts
  • 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/AccountsPane.tsx
  • 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/components/settings/accounts-search.test.ts
  • src/renderer/src/components/settings/accounts-search.ts
  • src/renderer/src/i18n/locales/en.json
  • src/shared/default-global-settings.ts
  • src/shared/global-settings-types.ts
  • src/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.

Comment thread src/main/claude-accounts/claude-account-auto-switch.ts
…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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 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 win

Track the read timestamp only for eligible candidates.

When a non-candidate inactive account is fetching, decide can request a candidate read, but fetchInactiveClaudeAccountsOnOpen() returns because another inactive fetch is already running. The current listener has already set lastUsageReadAt, so the eligible candidate can remain stale until the 15-minute interval expires. Limit the timestamp update to the same candidate set used by decide.

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
📥 Commits

Reviewing files that changed from the base of the PR and between 3dfbfc1 and dd626ea.

📒 Files selected for processing (2)
  • src/main/claude-accounts/claude-account-auto-switch.test.ts
  • src/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.
@adifsgaid

Copy link
Copy Markdown
Author

Small update since I opened this. I kept poking at it and pushed a few follow-ups:

  • CodeRabbit's point about a skipped usage read (thread above).
  • A polled usage reading wasn't tagged with its account, so the switch only fired while a session was actively posting usage. That was a real bug in my first version. Fixed in dd2aa09, with a test at the fetch level this time.
  • Usage from a window that has already reset is ignored now (same root as fix(rate-limits): ignore stale Claude statusline snapshots from idle sessions #23421).
  • A few edge cases: the default WSL distro, accounts inside Retry-After, and asking the usage service for a read from inside its own publish loop.

@nwparker @Jinwoo-H @brennanb2025 could one of you approve the workflow run when you have a minute? I ran the pr.yml gates locally, but I'd rather see it green on your runners than on my laptop.

And if this just isn't something you want in Orca, tell me and I'll close it. No hard feelings.

@bacaxnot

Copy link
Copy Markdown

+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 cswap auto as a systemd service). Having it in Orca would remove a second system that has to stay in sync with Orca's account handling.

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?

@f8team

f8team commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

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:

  • Switch at: 80%, 90%, 95% or "At limit", shown under your toggle once it's on. 90% stays the default, so nothing changes for anyone who doesn't touch it. The toggle's description names the chosen line, and the notification says how far the account had got.
  • Accounts to switch between: one switch per account, all on by default, so an account added later joins on its own. Orca never switches to or away from an account that's off, and picking it by hand still works. This covers "hold an account out of rotation" from [Feature]: Automatic account failover on rate limit for managed Claude/Codex subscription accounts #20512.

The policy is unchanged apart from taking the line as a parameter. Exclusion is one more filter next to needsSignIn/sharesRuntime, plus a stay when the selected account is off. Tests cover each guard; for each one I removed it and checked that a test went red.

Commit: f8team@757471f (one commit on top of bdbdda43; screenshots in light and dark from a headless e2e launch with example accounts are ready for the PR).

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 system may describe that other login. Worth keeping in mind if that issue lands a change to how coveredBySystemDefault is decided.

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.

@adifsgaid

Copy link
Copy Markdown
Author

@bacaxnot thanks, good to hear the defaults match what you already run.

On the SSH question: it runs wherever the accounts live. On an orca serve host it runs on the server with that server's accounts, so it should keep going with the desktop closed (I don't have that setup, so I haven't tried it). The catch is that this PR hides the toggle when Settings shows a remote server's accounts, and there's no CLI command for it either, so right now you can't actually turn it on there. That's a gap on my side. I'd rather close it in a small follow-up than grow this PR, unless a maintainer would rather have it here. With plain SSH the accounts live on the desktop, so it works as is.

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.

@adifsgaid

Copy link
Copy Markdown
Author

@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 coveredBySystemDefault gets decided, the system check here should follow it. I'll keep an eye on it.

@adifsgaid

Copy link
Copy Markdown
Author

@bacaxnot the remote server part is up as #29060, stacked on this one. With it the toggle shows when Settings is looking at a server's accounts, and it turns the switch on for that server. The browser client had the same gap and it's fixed there too.

@adifsgaid

Copy link
Copy Markdown
Author

@bacaxnot the other half of what cswap auto does, moving sessions that are already running, is up as #29802. After a switch it reopens each Claude on the new account with claude --resume once its turn ends, either from a button in the Claude menu or on its own for idle sessions in background tabs. I tried it with two real accounts last night and the conversation carried over fine.

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

Copy link
Copy Markdown
Author

Merged main in. #29898 and #29209 changed the same files, so a few things moved:

  • The auto-switch now starts in createProviderAccountServices with the other account services, so orca serve builds it too. The desktop notification comes in as a callback from startup, so nothing Electron lands in the shared module.
  • @Jinwoo-H with feat(accounts): offer to restart running Claude sessions on the new account after a switch #29898 in, I hooked the automatic switch into your setting. Main tells the renderer which accounts it switched between, and the renderer runs handleRunningClaudeSessionsAfterSwitch the same way the menu does. So Ask / Always restart / Never restart apply to an automatic switch too. With Always restart that's the whole flow people wanted from cswap: the account runs low, Orca switches, and the running sessions resume on the new one.
  • The switch reports the account's own WSL distro, the way the menu does, so sessions in that distro are found.

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.

This branch has not been deployed

No deployments
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.

[Feature]: Automatic account failover on rate limit for managed Claude/Codex subscription accounts

3 participants