---
title: Account Pool (multiple Claude accounts + exhaustion alert)
---

# Account Pool (multiple Claude accounts + exhaustion alert)

## What it is

Omniscio can hold **more than one Claude login** and silently pick the best one for each session spawn based on usage headroom. When every tracked account has hit its 5-hour **and** 7-day cap (including any remaining extra-usage credits), Omniscio doesn't just fail — it surfaces an amber **"All accounts are at their usage limit"** banner at the top of the sessions sidebar, fires a 15-second in-app toast with a **View Accounts** button, and pings the OS notification center so you know to add capacity before queued sessions stall.

**How a spawn actually picks an account depends on one setting, and the default matters.** With **"Distribute sessions across accounts"** ON — which is the **default** (`distributeSessionsAcrossAccounts`, `DEFAULT_SETTINGS` sets it `true`, and a one-shot migration turned it on for existing installs) — the pick runs through the **load balancer**, which spreads new sessions across logins with capacity. **The load balancer never pins**, and it weighs more than raw utilization: standing sessions per account, in-flight spawns, a per-account ceiling, hot-account avoidance, and upcoming reset-window harvesting. See [distribute-sessions-across-accounts.md](distribute-sessions-across-accounts.md).

With that setting **OFF**, the pick falls back to the older **tiered** list: pinned account first (if it has capacity) → OAuth logins with primary capacity (sorted freshest-utilization-first) → OAuth logins relying on extra-usage credits → API-key accounts (only if `allowApiKeySessionSpawn` is enabled). The load balancer also **delegates** to this tier list when its own candidate pool comes up empty — so the tiers still describe the fallback path, just not the everyday one. If every tier is empty, the exhausted alert fires.

## Where to find it

### How to use it

1. **Add accounts.** Settings → **Accounts** → **Log In with Anthropic** (OAuth) for each extra Claude.ai subscription you want in the rotation. You can also paste an API key, but API-key accounts are the fallback of last resort and the `allowApiKeySessionSpawn` setting gates whether they can spawn sessions at all (default: off).
2. **(Optional) Pin a preferred account.** In the Accounts list, set one account as **Pinned** — the tiered picker will always try it first if it has primary capacity, falling through to the next-best when it hits its cap. **Pinning only applies when "Distribute sessions across accounts" is OFF** — the load balancer (the default) deliberately never pins, because pinning and spreading are opposite goals.
3. **Just work.** You don't have to pick an account per session. On the default (load-balanced) path, Omniscio spreads new sessions across logins that have capacity, factoring in how many sessions each is already carrying. On the tiered path it instead scores each account by `max(5h_utilization, 7d_utilization)` and uses the freshest one. Either way, running sessions don't move between accounts mid-turn; the pick happens at spawn time and on **Continue** after a rate-limit recovery (a session can silently resume on a different account — see [session-stuck-in-needs-you.md](session-stuck-in-needs-you.md) rate-limited case).
4. **Read the usage bar.** Each account row and the session-panel usage widget show a percentage bar. Over 100% turns red; the tooltip shows an **absolute reset time** ("Thursday 5:00 PM") rather than a relative countdown so you can actually plan.
5. **When the exhausted banner appears.** Click **View Accounts** to add another account, wait for one of the reset times, or dismiss the banner (X). A dismissed banner stays dismissed for that exhaustion event — if any account regains capacity and then a new exhaustion happens later, the banner comes back. An OS notification only fires once per 30-minute window so you don't get spammed.
6. **Muting.** The exhausted alert respects the global notification controls — `notificationBehavior: 'never' | 'backgrounded' | 'always'` and `silenceUntil` — same as other OS toasts. The in-app banner itself cannot be globally muted (it's load-bearing UX: you need to know sessions can't spawn).

## How it behaves

### When a login goes bad (dead-login quarantine + failover)

Separate from running out of capacity, a login can go **dead** — its saved credential is no longer valid and even a token refresh is rejected (you'd typically have to sign in again). Omniscio handles this automatically:

- **Quarantine.** When a refresh is rejected twice in a row on the same token, Omniscio marks that login dead and **every account picker skips it** from then on, so new sessions never spawn against a credential that can only fail. (The first rejection is held, not acted on, in case another refresh had already replaced the token.) Signing in again clears the quarantine automatically. The 15-minute background refresh never marks a login dead on its own — once it has seen the same token rejected twice, it simply stops re-sending that token for an hour at a time, and in the log such a skipped attempt reads `skipped_known_dead`, not `dead_token`.
- **Failover.** Any sessions that were stuck on the dead login are **moved onto a healthy login** and resume on their own — you don't have to retry them. (Omniscio won't loop forever on a dead credential.)
- **Idle heads-up.** Even if you're not actively using a login, if Omniscio notices in the background that it has gone dead, it surfaces a re-login prompt **proactively** — so you can fix it before your next session fails instead of being surprised mid-work.
- **Auto-resume on re-login.** Once you sign that account back in, its sessions pick back up automatically.
- **Signing back in without doing it by hand.** If you hold a pool of accounts, Settings → Accounts offers **Sign them back in** — it drives the whole sign-in in your real Chrome, reads the sign-in email from your connected Google account, and captures each account back into the pool; you only do the two clicks Cloudflare and the consent screen insist on. See [account-relogin.md](account-relogin.md).
- **Nowhere healthy to go.** If a session needs to move but **every** login is dead or capped, it lands in a visible **recovery-failed** state (orange dot in the inbox) rather than silently idling — your cue to add capacity or re-authenticate.
- **A busy computer can't log you out (2026-09-28).** Every sign-in and every token refresh is sent from a small background process of its own, so when Omniscio's main process is frozen or overloaded, Anthropic's reply still gets saved. Before, a refresh that ran past its 10-second limit while the app was frozen could throw away the only copy of the new login, and the account dropped to "needs re-login" soon after. If a sign-in genuinely runs out of time, it now says **Sign-in timed out** instead of "try again in a moment". Engine detail: [auth-login-contract.md](/.claude/memory/contracts/auth-login-contract.md) `token-requests-run-in-their-own-process`.

### How it works

The pure selection logic is in [/src/shared/account-pool.ts](/src/shared/account-pool.ts): `pickBestAccount()` walks a pinned → primary-capacity → extra-capacity → api-key tier list and returns the first capable account, or `null` if every tier is empty. `hasPrimaryCapacity()` requires both the 5-hour and 7-day utilization under 100%; `hasExtraCapacity()` checks whether the account has extra-usage credits enabled with remaining balance; `primaryScore()` is `max(5h, 7d)` so a 99% 5-hour account loses to an 80% 7-day account even if its 7-day number is fresher. The `allowApiKeySessionSpawn` gate is **enforced upstream**, not inside `pickBestAccount` — the process manager calls `filterAccountsForSpawn()` from [/src/main/process/api-key-guard.ts](/src/main/process/api-key-guard.ts) FIRST to drop api-key accounts when the setting is off, so the pool's api-key tier only sees accounts that are already eligible to spawn.

The process manager calls this on every spawn — a synchronous `pickActiveAccount()` ([/src/main/process/account-selection.ts](/src/main/process/account-selection.ts)) reads the cached usage snapshot and kicks off a background refresh so the next spawn sees fresh numbers. **Which picker it delegates to is decided per-call by `isDistributeSessionsEnabled(getSettings())`**: ON (the default) → `pickLoadBalancedAccount()` in [/src/shared/account-pool-load-balance.ts](/src/shared/account-pool-load-balance.ts), which passes **no** `pinnedAccountId` and itself falls back to `pickBestAccount` when its candidate pool is empty; OFF (or `AMC_DISABLE_DISTRIBUTE_SESSIONS=1`, the operator rollback kill switch) → `pickBestAccount()` and the pinned-first tier list above. If the pick returns `null`, the rate-limit recovery service ([/src/main/services/rate-limit-recovery-service.ts](/src/main/services/rate-limit-recovery-service.ts)) calls `emitAllExhaustedAlert()`, which always pushes the IPC event (so the banner shows instantly) but only triggers the OS notification when a 30-minute cooldown has elapsed. `clearAllExhaustedState()` fires the inverse push when any account recovers capacity, so the banner auto-hides globally without user action. `countActionableSessions()` tallies the queued/waiting count included in the banner copy ("N sessions waiting").

**Rate-limited accounts drop out of the pool (2026-07-07).** Separate from the 5h/7d usage tiers above: the moment an account is seen rate-limited — the usage-stats poll gets a **429** (the same "Rate limited (429)" state the account row shows), or a session on it hits its cap — Omniscio drops that account from the picker via the in-memory **account breaker**, and auto-returns it when the limit's window clears (the per-account `Retry-After`, capped 5 min). A rate-limited account is a **strict** skip: the picker never spawns onto it, even as a last resort. So when _every_ account is rate-limited the pick returns `null` and the session **parks** (`needs_you` + `rate_limited`) and auto-resumes on the reset timer / next usage poll — instead of being shoved onto a limited account to 429 again. (This matters because a rate-limited account otherwise reads as _unknown_ usage, which `primaryScore` treats optimistically as 0% "fresh" — so without both the breaker arming AND this strict skip, a rate-limited account would rank top of the pool and get _preferred_.) Engine detail + invariants: [account-switch-contract.md](/.claude/memory/contracts/account-switch-contract.md).

**Burst vs. cap — a healthy account isn't benched for hours (2026-07-22).** A session-turn 429 on an account that _still has usage headroom_ (both the 5-hour and 7-day meters under 100%) is a short-lived **burst** throttle — Anthropic's short-term request-rate limit from many requests at once (e.g. a subagent fan-out), NOT the usage cap the 429's far reset time describes. So Omniscio benches that account only ~60 seconds instead of until the far reset, and — the moment a usage poll re-confirms it has headroom — returns it to the pool early. That keeps an account with plenty of quota left from being stranded out of rotation for hours over a momentary throttle (the "sessions waiting for capacity while accounts are free" case). Genuinely capped accounts (a meter at 100%) still bench until their real reset. Both behaviors are kill-switched. Engine detail: [recovery-storm-control-contract.md](/.claude/memory/contracts/recovery-storm-control-contract.md) `a-headroom-429-is-a-short-burst-bench` / `a-healthy-usage-poll-re-admits-early`.

**Fleet-wide meter throttle — a healthy account isn't benched when Anthropic throttles our usage _polling_ (2026-07-23).** Distinct from a per-account cap: Anthropic/Cloudflare can rate-limit Omniscio's own **usage-meter poll** (the lightweight call behind the percentage bars) across many accounts at once — a `429` that means "the meter is temporarily unreadable," not "this account is out of capacity." Benching accounts off that (as the 2026-07-07 rule did unconditionally) strands healthy accounts: on 2026-07-23 it benched ~30 accounts, one sitting at **3%** usage, while real capacity was abundant. So when several accounts' polls get throttled together **with no usable `Retry-After`**, Omniscio recognizes a fleet-wide meter throttle, **keeps those accounts in the pool**, and shows a neutral **"Usage temporarily unavailable"** on the row instead of "Rate limited (429)" (the renderer also stops auto-retrying that state so it can't re-hammer the throttled endpoint). A single account's poll `429` — or one carrying a real reset time — still benches, so the 2026-07-07 crash-on-spawn signal is preserved. Kill-switched (`AMC_DISABLE_USAGE_POLL_FLEET_THROTTLE_GUARD`). Engine detail: [recovery-storm-control-contract.md](/.claude/memory/contracts/recovery-storm-control-contract.md) `a-fleet-wide-throttle-does-not-bench`.

On the renderer, push events land on `IPC.ACCOUNT_POOL_EXHAUSTED` (defined in [/src/shared/ipc-channels/account.ts](/src/shared/ipc-channels/account.ts) as `'account:pool-exhausted'`) with a Zod-validated payload `{ exhausted, at, trackedSessionCount, message }`. The [/src/renderer/src/stores/account-pool-exhausted-store.ts](/src/renderer/src/stores/account-pool-exhausted-store.ts) Zustand slice holds the state plus a `dismissedAt` timestamp; the selector `selectIsExhaustedVisible()` is true only when `exhausted && at > dismissedAt`. [/src/renderer/src/hooks/useAccountPoolExhaustedAlert.ts](/src/renderer/src/hooks/useAccountPoolExhaustedAlert.ts) mounts the listener and fires the toast only when the alert is fresh (`at > previousAt`) so a backend re-push doesn't re-toast. The banner itself is [/src/renderer/src/features/dashboard/AccountPoolExhaustedBanner.tsx](/src/renderer/src/features/dashboard/AccountPoolExhaustedBanner.tsx), mounted above the sidebar in [/src/renderer/src/features/dashboard/SessionsSidebar.tsx](/src/renderer/src/features/dashboard/SessionsSidebar.tsx) so it's visible across every sidebar mode.

Full design record and invariants: [account-pool-exhaustion-alert postmortem](/.claude/memory/postmortems/account-pool-exhaustion-alert-postmortem.md). Related: [auto-account-switch postmortem](/.claude/memory/postmortems/auto-account-switch-postmortem.md) covers the silent cross-account resume on rate-limit recovery.

## Related

- [distribute-sessions-across-accounts.md](distribute-sessions-across-accounts.md) — the load balancer that is the DEFAULT spawn picker (what it weighs, and how to turn it off)
- [session-stuck-in-needs-you.md](session-stuck-in-needs-you.md) — rate-limited sessions use the pool to auto-resume on a different account
- [notifications-and-silence.md](notifications-and-silence.md) — global `notificationBehavior` + `silenceUntil` gate the OS notification (but not the banner)
