Repository navigation
Conversation
…val and Orca refreshes
…store pinned PTYs at startup
Selection and removal checked for pinned terminals before the pinned launch's reservation existed, and a usage fetch could stage or refresh an account while a pinned launch seeded the same Keychain item. Host mutations, usage fetches and pinned reservations now claim an account with one synchronous check-and-set and hold it until they finish; a pinned launch waits out a usage fetch and refuses a switch or removal in progress.
…queue A pinned launch that met an in-flight usage fetch waited up to 40s inside the process-wide auth mutation queue, stalling every Claude launch and sync. The reservation is now taken (and waited for) before entering the queue; the queued part re-validates the account and the reservation is released on every failure.
… writes Re-auth replaced an account's managed credentials without the host-mutation claim, so it could overwrite the store a pinned --account Claude refreshes. It now refuses up front while pinned terminals hold the account and claims the account from the credential write through rollback.
…als still run on A host Claude keeps the account it started on and refreshes it through ~/.claude after the host switches away, so pinning that account with --account gave one single-use refresh chain two owners. Unpinned host Claude PTYs and structured children now record the account they started on (from the prepared auth provenance, else the host selection), restored across restarts, and a pinned reservation refuses while any of them is still live.
There was a problem hiding this comment.
ℹ️ No critical issues — the pinned-launch mechanism, its refresh-ownership guards, and the no-
--accountpath all check out. Two rough edges inline, plus a nitpick.
Reviewed changes
Read the full 79-file diff and ran the new suites (all green): src/main/claude-accounts (33 files / 275 tests), the new CLI tests for worker-start/worktree create --account, the runtime PTY pinned-spawn suite, the runtime startup/Agent-Teams suites, and claude-fetcher-managed-account-usage.
- Selector resolution —
--account <id|email>resolves on the worker host; exact id beats a case-insensitive email, ambiguity/unknown/WSL refused, and a selector naming the active account reportsisActiveOnHostand takes the normal path. - Pinned auth preparation —
CLAUDE_CONFIG_DIR+CLAUDE_SECURESTORAGE_CONFIG_DIRpoint at the managed auth dir; the config-dir helper linksprojects, mirrorssettings.json, and add-only merges.claude.jsonfirst-run/trust/oauth keys. macOS seeds the dir-scoped Keychain item and reads it back on drain behind identity + seed-hash proof. - Refresh ownership — a per-account registry makes pinned reservations mutually exclusive with host switch/remove/re-auth and with Orca usage fetches, tracks live pinned PTYs (persisted + reconciled against the daemon), and attributes ordinary host PTYs to the account they launched on.
- Plumbing — CLI flag +
claude.launch-account.v1capability gate; params threaded through worker-start and worktree-create into the runtime PTY spawn; Agent Teams forced in-process for an--accountleader; structured workers downgraded with reasonpinned_claude_account; SSH/remote/WSL and the rendererpty:spawnlane refused.
ℹ️ Nitpicks
src/main/runtime/runtime-worktree-startup-claude-account.ts:25— whenrequest.startupis set the throw reuses'--account requires --agent claude.'even though the agent may be Claude; a distinct message (e.g. "--accountcannot combine with an explicit startup command") would save a confusing debug.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| if (placement.claudeAccount) { | ||
| return downgraded('pinned_claude_account', vocabulary) | ||
| } |
There was a problem hiding this comment.
The pinned_claude_account downgrade fires for any --account, including a selector that names the account already active on the host. The PR's resolution rules say an active-account selector runs the normal path unchanged, but this decision runs before resolution and only sees the raw selector, so a structured-default user gets a terminal worker (and the “non-active account” detail) for --account <active>. The same raw-selector assumption makes buildRuntimeAgentTeamsLaunchPlan force in-process teammates for the active account too.
Technical details
# `--account <active>` is treated as pinned in mode/Agent-Teams decisions
## Affected sites
- `src/main/agent-launch/agent-launch-mode.ts:140` — downgrades for any non-empty `placement.claudeAccount`.
- `src/main/runtime/rpc/methods/orchestration-worker-start-mode.ts:60` — passes the raw `params.account` as `claudeAccount`.
- `src/main/runtime/orca-runtime-agent-teams-launch-plan.ts:35` — forces `in-process` for any `claudeAccountId`.
## Required outcome
- Either only downgrade / force in-process when the resolved account is not the active host account (resolution already exists: `resolveClaudeLaunchAccount(...).isActiveOnHost`), or document that `--account` is selector-level terminal-only regardless of which account it names so the receipt and the Agent-Teams mode stop contradicting the resolution rules.
## Open questions for the human
- Is the active-account case worth special-casing, or is the selector-level behavior intentional and just under-documented?|
|
||
| /** Claims an account for an Orca-side usage fetch; null while pinned launches hold it. */ | ||
| export function beginClaudeAccountUsageFetch(accountId: string): (() => void) | null { | ||
| const release = claimUnlessPinned(usageFetchesByAccountId, accountId) |
There was a problem hiding this comment.
beginClaudeAccountUsageFetch claims only against pinned reservations (claimUnlessPinned), so a usage fetch and a host mutation (switch/remove/re-auth) are not mutually exclusive even though both touch the same managed credentials — unlike every other pair the registry guards. During a re-auth, a concurrent usage fetch can read the pre-re-auth credentials, refresh them, and write them back over the freshly written login.
Technical details
# usage fetch vs host mutation are not mutually exclusive
## Affected sites
- `src/main/claude-accounts/claude-pinned-pty-registry.ts:94` — `claimUnlessPinned` checks `countClaudePinnedAccountUsers` but not `hostMutationsByAccountId`.
- `src/main/claude-accounts/claude-account-registration.ts:132-155` — re-auth holds the host-mutation claim across its writes.
- `src/main/rate-limits/claude-managed-account-usage.ts:47,74-87` — a claimed usage fetch refreshes and persists managed credentials.
## Open questions for the human
- Is host-mutation ↔ usage-fetch exclusion intentionally out of scope (a pre-existing race), or should `beginClaudeAccountUsageFetch` also refuse while `hostMutationsByAccountId` holds the account? If out of scope, a note in the registry JSDoc would keep the “one owner per account” claim honest.|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughThe change adds a Merge Risk: 🟡 Moderate · up to This change adds per-launch Claude account selection. After a restart in which the background session daemon fails to come up, managed accounts used by the earlier terminals can stay locked. Later 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation Issue Resolution Add Full details: Docstring CoverageExplanation Docstring coverage is 32.18% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 87 functions across 50 files. (29 skipped: 29 over the file limit.)
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. Comment |
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: 9fe5b56a-d9f4-443e-8868-8bc3050871dd
📒 Files selected for processing (79)
src/cli/handlers/claude-launch-account-flag.tssrc/cli/handlers/orchestration-worker-account-cli.test.tssrc/cli/handlers/orchestration/worker-launch-handler.tssrc/cli/handlers/worktree.tssrc/cli/index-worktree-create-claude-account.test.tssrc/cli/specs/core.tssrc/cli/specs/orchestration-worker-specs.tssrc/main/agent-launch/agent-launch-mode.tssrc/main/claude-accounts/claude-account-host-mutation.tssrc/main/claude-accounts/claude-account-registration.tssrc/main/claude-accounts/claude-account-selection.tssrc/main/claude-accounts/claude-account-service-account-selection.test.tssrc/main/claude-accounts/claude-account-service-reauth-pinned.test.tssrc/main/claude-accounts/claude-host-pty-accounts.test.tssrc/main/claude-accounts/claude-host-pty-accounts.tssrc/main/claude-accounts/claude-launch-account.test.tssrc/main/claude-accounts/claude-launch-account.tssrc/main/claude-accounts/claude-pinned-config-dir.test.tssrc/main/claude-accounts/claude-pinned-config-dir.tssrc/main/claude-accounts/claude-pinned-credentials.tssrc/main/claude-accounts/claude-pinned-pty-registry.test.tssrc/main/claude-accounts/claude-pinned-pty-registry.tssrc/main/claude-accounts/environment.tssrc/main/claude-accounts/live-pty-gate.test.tssrc/main/claude-accounts/live-pty-gate.tssrc/main/claude-accounts/runtime-auth-service-pinned-launch.test.tssrc/main/claude-accounts/runtime-auth-service.tssrc/main/claude-accounts/runtime-auth/runtime-auth-pinned-launch.tssrc/main/claude-accounts/runtime-auth/runtime-auth-readback.tssrc/main/claude-accounts/runtime-auth/runtime-auth-sync.tssrc/main/claude-accounts/runtime-auth/runtime-auth-types.tssrc/main/daemon/daemon-provider-init.tssrc/main/ipc/pty-runtime-claude-account-spawn.test.tssrc/main/ipc/pty-spawn-env-terminal-basics.test.tssrc/main/ipc/pty/host-env/types.tssrc/main/ipc/pty/ipc/spawn-commit.tssrc/main/ipc/pty/runtime/spawn-claude-account.tssrc/main/ipc/pty/runtime/spawn-commit.tssrc/main/ipc/pty/runtime/spawn-preflight.tssrc/main/ipc/pty/runtime/spawn-state.tssrc/main/ipc/pty/runtime/spawn.tssrc/main/rate-limits/claude-fetcher-managed-account-usage.test.tssrc/main/rate-limits/claude-managed-account-credentials.tssrc/main/rate-limits/claude-managed-account-usage.tssrc/main/runtime/orca-runtime-agent-teams-launch-plan.test.tssrc/main/runtime/orca-runtime-agent-teams-launch-plan.tssrc/main/runtime/orca-runtime-create-managed-worktree.tssrc/main/runtime/orca-runtime-create-terminal-desktop.tssrc/main/runtime/orca-runtime-create-terminal.tssrc/main/runtime/rpc/methods/orchestration-worker-start-mode.tssrc/main/runtime/rpc/methods/orchestration/worker/local-worker-start.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-claude-account-placement.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-claude-account.test.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-launch-preferences.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-start-agent-placement.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-topology.tssrc/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.tssrc/main/runtime/rpc/methods/orchestration/worker/workers.tssrc/main/runtime/rpc/methods/worktree-create-args.tssrc/main/runtime/rpc/methods/worktree-create-claude-account-args.test.tssrc/main/runtime/runtime-folder-worktree-create.tssrc/main/runtime/runtime-local-worktree-terminal-startup.test.tssrc/main/runtime/runtime-local-worktree-terminal-startup.tssrc/main/runtime/runtime-managed-worktree-create-types.tssrc/main/runtime/runtime-pty-controller-contract.tssrc/main/runtime/runtime-terminal-contracts.tssrc/main/runtime/runtime-worktree-startup-claude-account.test.tssrc/main/runtime/runtime-worktree-startup-claude-account.tssrc/main/startup/main-process-account-services.tssrc/main/startup/main-process-ready-foundation.tssrc/main/startup/main-process-runtime-launch.tssrc/main/startup/main-process-runtime-service.tssrc/main/startup/main-window-core-services.tssrc/main/window/attach-main-window-services.tssrc/shared/agent-launch-intent.tssrc/shared/protocol-version.tssrc/shared/rpc-contract/orchestration-worker-start-params.tssrc/shared/rpc-contract/worktree-create-params.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| // Why: `--account` must not pin an account a surviving host Claude still refreshes. | ||
| setClaudeActiveHostAccountResolver(() => | ||
| getSelectedClaudeAccountIdForTarget(store.getSettings(), { runtime: 'host' }) | ||
| ) | ||
| const hostClaudePtyAccountsPath = join(app.getPath('userData'), 'claude-host-pane-accounts.json') | ||
| attachClaudeHostPtyAccountPersistence( | ||
| createClaudeHostPtyAccountFilePersistence(hostClaudePtyAccountsPath) | ||
| ) | ||
| seedHostClaudePtyAccounts( | ||
| persistedClaudePtyIds, | ||
| readClaudeHostPtyAccountsFile(hostClaudePtyAccountsPath) | ||
| ) | ||
| // Why: same restart hazard for `--account` PTYs, whose accounts the global gate never covers. | ||
| const pinnedClaudePtyRegistryPath = join( | ||
| app.getPath('userData'), | ||
| 'claude-pinned-pane-accounts.json' | ||
| ) | ||
| seedPinnedClaudePtysFromPersistence(readClaudePinnedPtyRegistryFile(pinnedClaudePtyRegistryPath)) | ||
| attachClaudePinnedPtyPersistence( | ||
| createClaudePinnedPtyFilePersistence(pinnedClaudePtyRegistryPath) | ||
| ) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
# Who drives seed confirmation, and does a non-daemon startup ever reach it?
rg -n -C5 'reconcileSeededClaudeLivePtys|confirmSeededPinnedClaudePtys|confirmSeededClaudeLivePtys' src/main
rg -n -C5 'initDaemonPtyProvider|daemonProviderInit|LocalPtyProvider\(' src/main/daemon src/main/startupRepository: stablyai/orca
Length of output: 41933
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- daemon provider initialization ---'
sed -n '45,230p' src/main/daemon/daemon-provider-init.ts
printf '%s\n' '--- startup PTY provider wiring ---'
sed -n '95,165p' src/main/startup/main-process-pty-startup.ts
printf '%s\n' '--- pinned registry seed/count and reservation behavior ---'
sed -n '150,280p' src/main/claude-accounts/claude-pinned-pty-registry.ts
printf '%s\n' '--- host account seed/count and reservation behavior ---'
rg -n -C8 'seedHostClaudePtyAccounts|countHostClaudePtysForAccount|host-sessions|attachClaudeHostPtyPersistence' src/main/claude-accountsRepository: stablyai/orca
Length of output: 34888
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- startup service orchestration ---'
rg -n -C12 'function startFirstWindowStartupServices|startFirstWindowStartupServices|startDaemonPtyProvider|LocalPtyProvider|persistent' src/main/startup src/main/ipc src/main/daemon
printf '%s\n' '--- daemon startup failure and fallback handling ---'
rg -n -C12 'daemon-pty-provider|initDaemonPtyProvider|setLocalPtyProvider|DegradedDaemonPtyProvider|fallback' src/main/startup src/main/daemonRepository: stablyai/orca
Length of output: 45544
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- defining files ---'
rg -l 'function startFirstWindowStartupServices|export function startFirstWindowStartupServices|const startFirstWindowStartupServices' src/main
printf '%s\n' '--- startup service references ---'
rg -n 'startFirstWindowStartupServices|startDaemonPtyProvider' src/main/startup --glob '*.ts' --glob '!*.test.ts'
printf '%s\n' '--- persistent-provider state references ---'
rg -n -C8 'daemonOwnsFreshPersistentPtys|LocalPtyProvider|setLocalPtyProvider' src/main/startup src/main/daemon --glob '*.ts' --glob '!*.test.ts'Repository: stablyai/orca
Length of output: 37803
🏁 Script executed:
#!/bin/bash
set -e
cat -n src/main/startup/first-window-startup-services.ts | sed -n '1,170p'Repository: stablyai/orca
Length of output: 4795
Clear persisted Claude PTY seeds after a non-persistent startup.
initializeReadyFoundation seeds both registries from persistence. reconcileSeededClaudeLivePtys runs only after daemon initialization completes. If daemon initialization fails before reconciliation, or if session listing fails, the seeds remain.
Those seeds can keep accounts blocked after the previous local PTYs have exited. The host registry counts seeded entries, and reserveClaudePinnedAccount then rejects later --account launches with the host-sessions conflict.
When startup selects a non-persistent provider, clear both registries through their existing confirmation functions with an empty alive-session set. Keep the seeds when a persistent daemon may still own sessions or when daemon listing fails.
|
Thanks for this, @fardenghi. The per-launch primitive is exactly what per-project accounts needed. I built the GUI layer on top of it: each project can remember its Claude account (Settings → Repository), with an optional "which account?" prompt on launch, and every desktop launch path honors it (new tab, ⌘⌥T, New Workspace, resume/restore, paired hosts). It reuses your pinned path for both spawn lanes, including the renderer A few things I ran into while building and testing on macOS with two real subscriptions:
Thanks again, and let me know if you'd like the layer structured differently. |

ELI5
Orca can keep several Claude accounts signed in, but only one of them is "the" account on a machine at a time, so every Claude agent Orca starts uses that one. This PR lets you say, per launch, "start this Claude agent on my other account":
--account <id|email>. That agent runs on the account you named, the rest of Orca keeps using the active account, and nothing gets switched. Two Claude agents on two accounts can now work side by side. Launches without--accountbehave exactly as before.What Changed
Before: Claude account selection is global per host. Activating an account writes its credentials into the shared
~/.claude, and switching is blocked while Claude terminals are live. A script or coordinator agent can't say "start this worker on account X". The only workaround is a rawCLAUDE_CONFIG_DIR=… claudeterminal, and Orca doesn't recognize that as a managed agent: it has no hooks, noworker_doneand no account safety. See #8770.After:
Launch receipts report the resolved account:
account: { id, email, mode: 'pinned' | 'active' }.Mechanism. This is a "pinned launch", scoped to the host runtime lane.
--accountaccepts an id, or an email matched case-insensitively.orca account list.CLAUDE_CONFIG_DIRandCLAUDE_SECURESTORAGE_CONFIG_DIRset to that account's existing managed auth dir. Auth env is stripped the same way as on the WSL managed path. This is the same shape as the WSL path and theinactive-previewrate-limit path, so no new credential location is introduced..credentials.jsondirectly. It is the only store, so nothing is copied or read back.Claude Code-credentials-<sha256(dir)[:8]>). The unscoped legacy item is never touched. When the account's last pinned PTY exits, the item is read back into the managed store and deleted. The read-back runs only if the managed copy is unchanged since the seed, so a re-auth in between wins, and the identity matches. A small marker next to the auth dir holds a sha256 of the seeded blob (never the secret) so a crash is recovered at the next use.await. Host switch, remove and re-authenticate claim the account for their whole operation, including the filesystem and keychain work. Each side refuses while the other holds the claim, with "in use by N terminals launched with --account". Re-auth refuses up front, before any browser login opens.claudepreview) claims the account for the whole fetch. A pinned launch that meets an in-flight fetch waits for it, up to a bound, outside the process-wide auth queue, so unrelated launches and syncs are never delayed. While pinned, the fetch takes a read-only path (on macOS it reads the session's scoped item).--accountnever consult this record.markClaudePtyExited), persisted in<userData>/claude-pinned-pane-accounts.json, and confirmed against the daemon on restart.projects/is linked to the host'sprojects/(a junction on Windows), so resume, session search and usage scanners see one transcript store.settings.jsonis copied from the host, so Orca's hooks and the statusline load. Without it,worker-startwould end inturn_unobserved..claude.jsongets an add-only merge of first-run and trust keys plusoauthAccount, so the pane doesn't stop on onboarding or trust prompts.claude.launch-account.v1lets the CLI refuse--accountagainst an older host. Without it, an older host would silently launch on its active account.Out of scope (explicit errors, not silent fallbacks):
--on/remote runtimes and WSL-runtime accounts.pinned_claude_account.pty:spawnlane refuses a pinned launch.With Agent Teams enabled, a pinned launch uses in-process teammates, which share the pinned credentials, instead of separate-terminal teammates that would be prepared from the active account.
Why
CLAUDE_CODE_OAUTH_TOKENinjection? Claude cannot refresh a token passed that way, and it puts a credential in the environment.Linked Issue
Fixes #8770
Related: #2284, #6921, #9456, #19900, #17796
Visual Proof
N/A: CLI/RPC-only change, with no renderer UI touched. Launch receipts gain an optional
accountfield.Testing
pnpm typecheckandpnpm lintpass, including the rpc params catalog, max-lines and localization checks.src/main/claude-accounts,src/main/rate-limits,src/main/ipc/pty*,src/cli,src/main/runtime/rpc/methods/orchestration,src/main/agent-launchand the new runtime tests.codex-fetcher-pty-settle.test.ts, which fails identically onmain, and one timing-sensitive test that passes in isolation on bothmainand this branch.pnpm test(on an earlier revision of this branch; after the review fixes only the targeted suites above were re-run, 0 new failures): 88,248 passed and 40 failed, in 23 files. None of the failures are in files this PR touches. Confirmed identical onmain:browser-manager-viewport-ownershipandcodex-fetcher-pty-settle. The rest are real-binary, e2e-release and long-timeout suites on an 8 GB machine.pnpm build: not run locally (8 GB machine); relying on CI.pty-runtime-claude-account-spawn.test.tsasserts that a launch without--accountprepares auth with the unchanged single-argument call. It sets no config-dir variables, and produces the same env and spawn options as before.worker-start --account <inactive>;/statusin the pane;Platforms: macOS (tests). Linux and Windows paths are covered by unit tests with a mocked platform; the Windows junction path is covered only through mocked
symlinkSync.AI Disclosure
Implemented with Claude Code (Anthropic Claude Opus 5.5) under my direction. That covers design research against the existing WSL,
inactive-previewand Codex per-pane precedents, the implementation, the tests and review passes. I reviewed the design and the diff.Review
AI review summary (security and correctness passes over the full diff):
darwin. Linux and Windows use the managed.credentials.jsondirectly. Theprojectslink is a junction on win32. Paths usenode:path.--on,runtime:hosts and WSL targets are refused before any worktree or dispatch row is created.--account. Agent Teams uses in-process teammates for pinned launches.--accountpath. Pinned-dir preparation runs once per pinned launch. Crash markers are read once at startup.Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Known limitations, documented as follow-ups:
CLAUDE_CONFIG_DIRoverrides the pin.CLAUDE.md, agents, commands, skills, plugins and MCP config from~/.claudeare not inherited by the pinned dir; onlyprojectsandsettings.jsonare.settings.jsonis re-copied from the host on each pinned launch.Possible follow-ups:
orca terminal create --account;worker-startandworktree createcurrently callgetAccountsSnapshot().This PR does overlap with open work: #22068 touches the same
spawn-preflightauth call, and #22510 touchesruntime-auth-sync. I kept the edits at those points minimal and am happy to rebase behind either.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred) (lint and typecheck pass; see Testing for the pre-existing test failures; build is left to CI)