Repository navigation
feat(accounts): keep a Claude or Codex agent on its account - #28684
QichangZheng wants to merge 35 commits into
Conversation
A Codex pane's launch record gains an optional `kept` flag. The stale-pane sweep skips kept records, so a kept pane is never offered the "Account switched" prompt, and registry trimming never drops a kept record. `setCodexPaneKept` refuses against a registry it could not read instead of queuing a record it never saw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Keep old account" in the Account switched prompt used to delete the pane's launch record. It now marks the pane kept, so the answer survives restarts and the record still says which account the pane runs on. Panes whose home cannot stay on an account (the shared CODEX_HOME mirror, WSL panes, records from before route provenance) keep today's behaviour: the record is dropped. The channel becomes codexAccounts:keepStalePanes and also carries the pane key, so the host can keep the pane's conversation as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent Status rows gain an optional runAccount facet: which Claude or Codex account a local terminal agent runs on, and whether a person kept it there. The hook server is the only writer. republishRunAccount restates a row with a new facet and nothing else: its clocks, observation and state stay as they were, it is persisted with the row, and only the renderer is told, so plugin listeners, the session recorder and paired clients see no change. Later rows of the same agent keep the facet; a different agent in the pane drops it. Hydration validates it and keeps it only for local rows. The paired-client projections (pickParsedAgentStatusPayload) never copy it. In the renderer a host row's facet is copied as is; a renderer-local row (an OSC repaint) keeps the facet the host last sent for that agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host-side producer writes the runAccount facet onto local Claude and Codex terminal rows. It never guesses: - Codex reads the pane's existing launch record (account, kept flag, whether its home can be kept). WSL lanes publish nothing. - Claude dates a session to the account it started on: the pane's own pointer for panes started on one, otherwise the selection at the moment this runtime saw the session start. A session that was already running when it was first seen keeps the facet an earlier runtime recorded, or gets none. The statusline folder each post reports then corrects it, and a folder no saved account owns withdraws the facet. Both spawn-commit paths note the pane's lane and whether it was a fresh spawn. Statusline posts are forwarded to the producer separately from the usage feed, alias-resolved, whether or not they carry usage. Providers with no saved accounts get no facet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the only new store this feature needs: a small index of conversations a person kept on an account, keyed by provider and session id, so a kept agent keeps its account after its tab closes and whenever Orca starts it again. It holds explicit keeps only, has no cap and no expiry, and the file does not exist until the first keep. An unreadable file is reported as unavailable rather than empty, queued changes wait until it can be read, a corrupt file is set aside, and a file from another version is left alone. Keeps follow the agent: a new session in a kept Codex pane is kept too, and a new Claude session inherits its predecessor's keep only when it provably runs on the kept account. A kept start that Claude reports under a new id is adopted the same way. Removing an account releases what was kept on it, so those agents follow the selection again. A startup pass drops keeps whose transcript has been definitively gone for over a week. "Keep old account" now keeps the pane's current conversation as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Codex resume can now be told which account a conversation is kept on. That account's home is resolved before any ranking, with no fallback: - a managed account must still be saved, on this host, and its home must pass the existing ownership check; an unreadable home is "temporarily unavailable", an untrusted one is refused, and the selection is never cleared because the kept account is not the selection; - System default resumes in the system home, and is refused while a custom CODEX_HOME is set rather than using the mirror, which would be a second copy of the login; - the rollout is moved into the kept home the same way Restart does; - a kept conversation whose transcript cannot be found is refused instead of starting fresh on the selection. Session History resumes of a kept conversation retarget to its account instead of the selection, moving the rollout there from a listed home when needed. Forks are new conversations and still follow the selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Claude agent started kept on an account gets its own which-account file, claude-profile-pointers/account-<id>, instead of the selection's. It is keyed by account, never by pane, so moving or closing tabs cannot unpin an agent, and only people who have kept an agent ever get one. Orca never deletes these files: a missing file reads as System default. Before an account's folder is removed, its file is pointed at an empty placeholder folder, so a `claude` typed again in such a pane reaches Claude's own sign-in there and never another account's login; a failed removal points it back. Whenever the selection file is written, existing account files follow their account's route (its own folder, or System default while that covers it). Launches on a kept account reuse the selection's preparation: the same first-setup wait, capped refresh and refusals, with the account's file written before the agent starts. The env never falls back to the selection's file. Follow panes and the shell functions are unchanged. The credential persistence ratchet names the new file writer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One synchronous resolver decides, for every Claude or Codex resume, whether the conversation was kept on an account. Both the window spawn and the runtime controller call it at the same point, so the two cannot drift: - nothing kept (and every relay pane, other agent or fresh start): the spawn takes exactly the calls it took before, with the same arguments; - kept: a Claude pane gets the account's own pointer and launch preparation, a Codex resume is prepared in the kept account's home even when the request was a restart to the selection, and the new pane's launch record is kept too; - a keep that cannot be honoured (unreadable index, another lane, an account that is no longer saved) refuses before anything starts. Both commit paths tell the run-account producer which account pointer a Claude pane started with and which conversation it resumed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tab hover card now names the account each Claude or Codex agent in the tab runs on, with one line per agent pane in split tabs, and what that means for its next start: kept on this account, kept from its next start (a Claude agent kept after it started; a `claude` typed by hand in that terminal still follows the selection), or following the selected account. The tab's right-click menu offers "Keep on <account>" per agent pane. It is disabled, with the reason, until the agent reports its session or while CODEX_HOME is set. Releasing a Codex pane re-checks it against the selection, so a pane left behind gets the usual Account switched prompt. Both read the host-published facet; the renderer keeps only labels and copy. The keep goes through a new agentAccounts:setKept channel. Web clients never see the facet, so they offer nothing and need no shim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Claude and Codex account switchers show "N terminal agents" on each of this host's account rows, counted from the host-published account on Agent Status rows whose tab is open. Chats publish no account, so they are not counted, and rows for another runtime's or a WSL lane's accounts show no count. The Claude switcher's footnote now says that agents kept on an account keep it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removing a Claude or Codex account releases the conversations kept on it, so those agents follow the selected account from their next start. The removal dialog now says so, with the number of this host's open terminal agents kept on that account, whenever there is at least one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the account line in the tab hover card, Keep on in the tab menu, "Keep old account", what a kept agent does through restarts, wakes and Session History resumes, the switcher counts, account removal, and where Keep is not offered. The Claude page explains the one difference: keeping an agent after it started applies from its next start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A kept Codex pane is not reported stale when the selection moves, while an unkept pane still is. A kept Claude conversation resumes on its account's own folder and pointer while another account is selected, and a plain terminal keeps following the selection. A probe standing in for `claude` prints which folder and pointer a launch reads; it starts nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tabsByWorktree: state.tabsByWorktree ?? EMPTY_TABS` counts as a renderer layout write. The store always holds the map, so pass it through as is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before Escape and a backdrop click shared the button's handler, so a stray key aimed at the terminal kept the conversation on the old account for good. They now dismiss the prompt and forget the pane's launch record, as before this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lemetry The window listener treated the restated row as a status event, so a Codex pane waiting for approval got a forced "action required" title with a bell: an unread badge and an OS notification whenever a keep changed or the first account was added. The republish is now flagged and only forwarded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A selection write rewrote every per-account pointer with no guard, so one file Orca could not rewrite (a directory, a read-only file) failed launches of agents nobody kept. Each rewrite is now caught and logged. - A kept launch during an account's removal skipped the selection and re-ran setup, recreating the folder being deleted. A retiring account now refuses kept launches, and its terminal env names only the pointer. - A removed account's pointer named one shared, writable placeholder folder, so a re-typed `claude` offered a sign-in inside Orca's data. It now holds a value that names no folder, which the `claude` function already refuses to run, and the removal dialog says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Statusline folders compare through normalizeRuntimePathForComparison, the key the Codex resume code already uses, instead of a second normalizer. - The kept-conversations index and the Codex pane registry share one retry backoff instead of two copies of the same timer. - Design walk-throughs in doc comments are cut to one line of why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le chain - /clear or /resume after a global switch gave the kept Claude conversation a new session id that was dated to the new selection, so it lost its keep and later resumed on the wrong account. A session change inside the same Claude process (same hook process identity) now inherits the process's account, and a statusline post that proves the folder re-runs the carry. A carried keep the statusline proves runs elsewhere is dropped. - Sessions that carry a keep on now record which keep they came from, so un-keeping any session of an agent releases all of them instead of leaving earlier ones silently kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refused kept resume never gets a terminal, and releasing a Codex keep
required one, so the refusal's own advice ("un-keep it") failed and the
conversation stayed refused for good. A release now needs no terminal: it
releases the conversation from the row, clears the pane flag only if a
launch record exists, and withdraws a facet nothing observes any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… resume A list of kept agents written by a newer Orca made this build refuse every Claude and Codex resume, kept or not, with "try again", which never helped. That file now reads as nothing kept and is never rewritten, and keeping from the tab menu says why it cannot. A file that cannot be read at all still refuses, but the message names the file and how to get past it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…prompt The "Account switched" overlay stayed up after the pane was kept from the tab menu, and its Restart then came back on the kept account, not the new one the overlay named. A successful keep now dismisses the pane's prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Passing the store's tabs through unchanged (for the layout-writer ratchet) handed undefined to the tab index in stores without tabs, which threw. The counter now reads a missing map as no open tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oved account does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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: 🔵 Low · up to
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
68216c22-a0d9-4a9f-878a-eb405d7c4e35
📒 Files selected for processing: 124
docs/site/content/docs/agents/claude-code.mdxdocs/site/content/docs/agents/codex-hot-swap.mdxsrc/main/agent-accounts/agent-account-keep-wiring.tssrc/main/agent-accounts/agent-account-keep.test.tssrc/main/agent-accounts/agent-account-keep.tssrc/main/agent-accounts/agent-launch-account.test.tssrc/main/agent-accounts/agent-launch-account.tssrc/main/agent-accounts/agent-run-account-adoption.tssrc/main/agent-accounts/agent-run-account-compute.tssrc/main/agent-accounts/agent-run-accounts-installed.tssrc/main/agent-accounts/agent-run-accounts.test.tssrc/main/agent-accounts/agent-run-accounts.tssrc/main/agent-accounts/kept-account-refusals.tssrc/main/agent-accounts/kept-agent-conversations.test.tssrc/main/agent-accounts/kept-agent-conversations.tssrc/main/agent-accounts/kept-codex-home.tssrc/main/agent-hooks/server-claude-statusline.test.tssrc/main/agent-hooks/server-last-status-hydrate-validation.test.tssrc/main/agent-hooks/server-run-account.test.tssrc/main/agent-hooks/server.tssrc/main/agent-hooks/server/server-lifecycle.tssrc/main/agent-hooks/server/server-persistence-validation.tssrc/main/agent-hooks/server/server-row-ownership.tssrc/main/agent-hooks/server/server-run-account-carry.tssrc/main/agent-hooks/server/server-run-account.tssrc/main/agent-hooks/server/server-status-identity.tssrc/main/agent-hooks/server/server-status-update.tssrc/main/agent-hooks/server/server-types.tssrc/main/claude-accounts/claude-credential-persistence-ratchet.test.tssrc/main/claude-accounts/claude-profile-account-pointers.test.tssrc/main/claude-accounts/claude-profile-account-pointers.tssrc/main/claude-accounts/claude-profile-account-route.tssrc/main/claude-accounts/claude-profile-router.tssrc/main/claude-accounts/runtime-auth-service-kept.test.tssrc/main/claude-accounts/runtime-auth-service.tssrc/main/codex-accounts/runtime-home-kept-resume.test.tssrc/main/codex-accounts/runtime-home-service-home-routing.tssrc/main/codex-accounts/runtime-home-service-state.tssrc/main/codex/codex-ai-vault-session-resume-kept.test.tssrc/main/codex/codex-ai-vault-session-resume.tssrc/main/codex/codex-pane-account-registry-kept.test.tssrc/main/codex/codex-pane-account-registry-mutations.test.tssrc/main/codex/codex-pane-account-registry-mutations.tssrc/main/codex/codex-pane-account-registry-types.tssrc/main/codex/codex-pane-account-registry.tssrc/main/codex/codex-stale-pane-accounts.test.tssrc/main/codex/codex-stale-pane-accounts.tssrc/main/ipc/agent-account-keep.test.tssrc/main/ipc/agent-account-keep.tssrc/main/ipc/codex-accounts-keep-stale-panes.test.tssrc/main/ipc/codex-accounts.tssrc/main/ipc/pty-claude-kept-launch.test.tssrc/main/ipc/pty-codex-kept-resume.test.tssrc/main/ipc/pty-kept-account-spawn.test-fixture.tssrc/main/ipc/pty/host-env/agent-account-launch.tssrc/main/ipc/pty/host-env/codex-home.tssrc/main/ipc/pty/host-env/codex-resume-kept.test.tssrc/main/ipc/pty/host-env/codex-resume.tssrc/main/ipc/pty/host-env/types.tssrc/main/ipc/pty/ipc/spawn-commit-persist.tssrc/main/ipc/pty/ipc/spawn-commit.tssrc/main/ipc/pty/ipc/spawn-env-codex.tssrc/main/ipc/pty/ipc/spawn-preflight.tssrc/main/ipc/pty/ipc/spawn-state.tssrc/main/ipc/pty/runtime/spawn-commit.tssrc/main/ipc/pty/runtime/spawn-preflight.tssrc/main/ipc/pty/runtime/spawn-state.tssrc/main/ipc/register-core-handlers/register-core-handlers.test.tssrc/main/ipc/register-core-handlers/register-core-handlers.tssrc/main/persist-retry-backoff.tssrc/main/startup/codex-session-resume-kept.test.tssrc/main/startup/codex-session-resume-launch.tssrc/main/startup/main-process-account-services.tssrc/main/startup/main-process-agent-run-accounts.test.tssrc/main/startup/main-process-agent-run-accounts.tssrc/main/startup/main-process-runtime-service.tssrc/main/startup/main-window-agent-status.tssrc/main/startup/main-window-core-services.tssrc/main/startup/main-window-structured-status-filter.test.tssrc/preload/api-types.tssrc/preload/api/agent-account-api.tssrc/preload/api/agent-account-keep-bridge.tssrc/preload/api/codex-accounts-bridge.tssrc/preload/index.tssrc/renderer/src/components/CodexRestartChip.tsxsrc/renderer/src/components/codex-restart-chip.test.tsxsrc/renderer/src/components/settings/KeptAgentsRemovalNote.tsxsrc/renderer/src/components/settings/accounts-pane-removal-dialogs.tsxsrc/renderer/src/components/settings/accounts-pane-removal-kept-agents.test.tsxsrc/renderer/src/components/status-bar/AccountAgentCount.tsxsrc/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsxsrc/renderer/src/components/status-bar/CodexSwitcherMenu.tsxsrc/renderer/src/components/status-bar/count-terminal-agents-by-account.test.tsxsrc/renderer/src/components/status-bar/count-terminal-agents-by-account.tssrc/renderer/src/components/status-bar/switcher-agent-count.test.tsxsrc/renderer/src/components/tab-bar/SortableTab.tsxsrc/renderer/src/components/tab-bar/SortableTabContextMenu.tsxsrc/renderer/src/components/tab-bar/TabAgentAccountLines.test.tsxsrc/renderer/src/components/tab-bar/TabAgentAccountLines.tsxsrc/renderer/src/components/tab-bar/TabAgentAccountMenuItems.test.tsxsrc/renderer/src/components/tab-bar/TabAgentAccountMenuItems.tsxsrc/renderer/src/components/tab-bar/TabHoverCard.tsxsrc/renderer/src/components/tab-bar/tab-agent-accounts.test.tssrc/renderer/src/components/tab-bar/tab-agent-accounts.tssrc/renderer/src/components/tab-bar/use-tab-agent-accounts.tssrc/renderer/src/components/terminal-pane/TerminalPaneRuntimePortals.tsxsrc/renderer/src/hooks/ipc-events/normalize-agent-status-event.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/store/slices/agent-status-contract.tssrc/renderer/src/store/slices/agent-status-live-entry-builder.tssrc/renderer/src/store/slices/agent-status-live-entry-run-account.tssrc/renderer/src/store/slices/agent-status-run-account.test.tssrc/renderer/src/web/preload-api/web-agent-accounts-api.tssrc/shared/agent-run-account.test.tssrc/shared/agent-run-account.tssrc/shared/agent-status-ipc-payload.tssrc/shared/agent-status-types.tssrc/shared/claude-profile-routing.tssrc/shared/claude-shell-function.test.tssrc/shared/claude-statusline-rate-limits.tstests/e2e/agent-account-keep-claude.spec.tstests/e2e/agent-account-keep-codex.spec.tstests/e2e/fixtures/claude-account-probe/claudetests/e2e/helpers/agent-hook-endpoint.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.
|
Hi @brennanb2025, thanks for picking this up. Before you dig into the diff, here is the whole plan this PR belongs to, so you can tell me early whether the direction works for Orca. Goal: agent-level account scheduling. Today Orca has one selected Claude account and one selected Codex account, and every agent follows it. The idea is to make the account a property of each agent, so different agents in one Orca can run on different accounts at the same time, and work can be spread across subscriptions instead of all agents moving together when one account runs low. I'd like to get there in three PRs, each one mergeable on its own and each building on the previous one:
Throughout: no login is ever copied or moved. Agents on one account keep sharing that account's folder/home as today, because refresh tokens rotate as one chain and copies would invalidate each other. This PR is step 1, and it's deliberately the foundation the other two need: the per-agent account on the Agent Status row, the kept mode, one resolver both spawn paths call, the per-account Claude pointer, and per-account Codex home resolution for resume. I've paused steps 2 and 3 until I hear from you. If the direction looks right, I'll continue; if you'd rather this land differently (for example split into a Codex PR and a Claude PR, or scoped down further), I'm happy to restructure. |
…he next read A keep or un-keep made while the kept-agents file could not be read waited for the next retry. A launch in between started on the selected account, and once the retries ran out nothing wrote the change down. The first read after the file is readable again now applies and writes the queued change; a restart before any such read still loses it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un-keeping from the tab menu reported success and dropped the kept marker while the newer Orca's list went on holding the keep. It now answers that this version cannot change the list, as keeping already did, and leaves the list alone. A Codex pane's own keep lives in its launch record, not in that list, and "Keep old account" can still set it there. Un-keeping such a pane still clears the record, so the tab menu can undo what the prompt kept. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unch registry Un-keeping first asked the launch registry whether the pane had a record, and an unreadable registry answered "no record". With the kept-agents list writable, un-keeping then reported success and dropped the kept marker while the launch record stayed kept, so the pane was kept again once the registry was readable. With the list owned by a newer Orca it answered read-only. It now goes by the release's own answer: an unreadable registry reports unavailable so the user can retry, and only a pane with no record is treated as having nothing in the registry to let go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@QichangZheng your step 3 is the thing I've been prototyping from the other side. #28044 switches the selected account before a limit, and the obvious gap (#19900) is that running agents don't follow. Rather than build a second version next to yours, I'd like to help with step 3 on top of step 1 once it lands, or leave it to you if you're already on it. A few things that came up while working through the cases, in case they save you time:
The cases are in one clickable HTML file if it helps (download it and open it, nothing to build): https://github.com/adifsgaid/orca/blob/prototype/resume-on-switch/src/renderer/src/lib/PROTOTYPE-resume-on-account-switch.html On step 2 and #28044: they overlap. #28044 moves the selected account, your step 2 picks an account for each new agent. @brennanb2025 if you prefer one shape, I'm happy to fold the #28044 policy ( |
|
@QichangZheng I opened #29802 for the switch-driven half of #19900: after you switch, it moves the running Claude sessions to the new account with It doesn't overlap with this one. Today it reads a session's account from its transcript path, all in one function ( |
Kept-account launches now ride main's shared spawn steps: both preflights resolve the account launch from the startup target before Claude auth, the kept Codex account reaches resume prep through resolvePtySpawnHostEnv (new optional keptAccount), and the kept-resume rules moved into the resume-launch factory the desktop and orcad share. Facet-only republishes still skip telemetry; titles and first-work renames now hang off the hook subscription, which they never reach. The window lane notes a pane's launch before seeding its resume identity, as the runtime lane already did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le from the tab menu The restored-row sweep retires a dead terminal's row to an identity row and clears the pane. The identity row keeps the account facet, but the window dropped it from identity rows and the renderer drops every non-Pi identity row, so the tab menu and hover card lost the agent. A refused kept resume then told the person to un-keep it from a tab menu item that was gone. - The hook server restates the facet after a retirement that keeps resume identity (after its pane clear), as a facet-only replay. - The window forwards the facet on identity rows, including a withdrawal. - The renderer mirrors identity-row facets in a small store; the tab menu, hover card and per-account counts read it. An identity row's facet wins until a newer status row takes the pane back, and a pane clear forgets it. Remote and stale identity rows never name an account. Status rows, the sweep itself and rows without a facet behave as before. The same path now shows a resumed pane's facet before its first turn. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keeps main's native-adoption resume preparation and passes the kept-home resolver through it; drops the Claude switcher footer note as main does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Claude agent kept on the outgoing account was counted as still running there, listed in the switch dialog and queued by Restart and Always restart, only to resume on its kept account again. The switch now leaves kept agents out of the count and the list, asks nothing when only kept agents remain, and a queued restart of a pane that became kept is dropped instead of relaunching it. Agents that follow the selection restart exactly as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t, and out of the switch restart An idle Claude terminal that outlived an Orca restart reattaches without posting anything, so the producer had no observation of it and withdrew the kept facet it had just been hydrated with. The switch dialog then counted, listed and restarted that kept agent again. A pane this runtime has not seen report now keeps the account an earlier runtime recorded, reading only the keep afresh; a fresh spawn still starts from nothing. A conversation kept on one account but running on another (resumed by hand in a selection-following terminal) showed as following, yet its restart resumes on the kept account. The facet now says so, and the switch leaves such an agent out like a kept one. Agents that follow the selection restart exactly as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@adifsgaid thanks, this is useful. Your cases match what I had in mind for step 3: only move panes that are Now that #29898 is on main, I've updated this PR so its switch dialog leaves kept agents alone: a kept agent stays on its account, and every other session follows the switch the way #29898 does today. I've also merged current |
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:
be31eff6-30c7-4256-961e-598318d8ae1a
📒 Files selected for processing: 20
docs/site/content/docs/agents/claude-code.mdxsrc/main/agent-accounts/agent-run-account-compute.tssrc/main/agent-accounts/agent-run-accounts.test.tssrc/main/agent-accounts/agent-run-accounts.tssrc/main/claude-accounts/claude-profile-router.tssrc/main/codex/codex-ai-vault-session-resume.tssrc/main/startup/main-process-agent-run-accounts.test.tssrc/main/startup/main-process-runtime-service.tssrc/renderer/src/components/codex-restart-chip.test.tsxsrc/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsxsrc/renderer/src/components/status-bar/switcher-agent-count.test.tsxsrc/renderer/src/components/terminal-pane/claude-detached-pane-restart.test.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/lib/agent-account-restart-startup.test.tssrc/renderer/src/lib/agent-account-restart-startup.tssrc/renderer/src/lib/claude-session-account-move.test.tssrc/renderer/src/lib/claude-session-account-move.tssrc/renderer/src/store/agent-identity-row-run-accounts.tssrc/shared/agent-run-account.test.tssrc/shared/agent-run-account.ts
🚧 Files skipped from review as they are similar to previous changes: 1
- src/renderer/src/i18n/locales/en.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| // Why: a kept conversation retargets to its own account, never to the selection. | ||
| const keptHome = | ||
| !args.fork && args.sessionId && args.executionHostId === LOCAL_EXECUTION_HOST_ID | ||
| ? options.resolveKeptCodexHome?.(args.sessionId) | ||
| : undefined | ||
| const keptOnSystemDefault = keptHome !== undefined && sameHome(keptHome, getSystemCodexHomePath()) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
rg -nP -C8 '\bcodexAiVaultSessionResumePreparation\b' --type=ts
rg -nP -C5 '\bprepareAiVaultSessionResume\s*\(' --type=ts -g '!**/*.test.ts'Repository: stablyai/orca
Length of output: 13328
Add a Codex agent check before the kept-home lookup.
The generic Session History resume path passes Claude and Codex requests to this preparer. Without the agent check, a non-Codex local session can call resolveKeptCodexHome with its session ID. This can apply a Codex keep entry to another agent or propagate keptAccountRefusals.indexUnavailable to that resume.
🐛 Suggested fix
const keptHome =
- !args.fork && args.sessionId && args.executionHostId === LOCAL_EXECUTION_HOST_ID
+ args.agent === 'codex' &&
+ !args.fork &&
+ args.sessionId &&
+ args.executionHostId === LOCAL_EXECUTION_HOST_ID
? options.resolveKeptCodexHome?.(args.sessionId)
: undefined📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Why: a kept conversation retargets to its own account, never to the selection. | |
| const keptHome = | |
| !args.fork && args.sessionId && args.executionHostId === LOCAL_EXECUTION_HOST_ID | |
| ? options.resolveKeptCodexHome?.(args.sessionId) | |
| : undefined | |
| const keptOnSystemDefault = keptHome !== undefined && sameHome(keptHome, getSystemCodexHomePath()) | |
| // Why: a kept conversation retargets to its own account, never to the selection. | |
| const keptHome = | |
| args.agent === 'codex' && | |
| !args.fork && | |
| args.sessionId && | |
| args.executionHostId === LOCAL_EXECUTION_HOST_ID | |
| ? options.resolveKeptCodexHome?.(args.sessionId) | |
| : undefined | |
| const keptOnSystemDefault = keptHome !== undefined && sameHome(keptHome, getSystemCodexHomePath()) |
The tab context menu imports both the agent account items and the new close-split item. SortableTab was at its 400-line limit on main, so the terminal tab's hover card (with its account lines) moves to its own TerminalTabHoverCard. Since stablyai#29884 a host spawn names its pane from its own tab and leaf ids, so the kept-launch host test passes those instead of ORCA_PANE_KEY in the caller's env. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…econds A millisecond mtime set through utimes can read back as 691.999 instead of 692, so the "writes only when the route changed" check failed now and then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ELI5
Each Claude or Codex agent tab now shows which account it runs on. Right-click the tab and choose Keep on <account>: switching the selected account no longer moves that agent, and when Orca restores, wakes or resumes it, it comes back on the same account. Agents you don't keep work exactly as before.
This is the first of three small steps toward running different agents on different accounts at the same time (the other two are under Why).
What Changed
What you see
CODEX_HOMEwas set.What a kept agent does. It stays on its account through global account switches, Orca restarts, sleep and wake, cold restore, and Session History resume. Un-keeping is the same checkbox.
Agents nobody kept. Both PTY spawn paths make the same calls with the same arguments as before, and no new file, timer, poll or process appears until someone presses Keep. The one always-on addition is in-memory: the account is worked out when an agent's status row changes (a provider with no saved accounts publishes nothing).
Mechanism
runAccountfacet (provider, lane, account,followorkept) written only by the host. The renderer reads it fromagentStatusByPaneKeyand owns only labels and copy. Paired clients, mobile andworktree psproject rows throughpickParsedAgentStatusPayload, which does not copy the new field, so nothing a remote peer receives changes. A facet-only restatement of a row leaves its clocks, observation and state alone and is forwarded to the renderer only (no title, bell, telemetry or plugin events).keptflag on the existing per-terminal record incodex-pane-account-registry; the stale-pane sweep and registry trimming skip kept records. A kept conversation resumes in its account's home through the existing restart-resume materialization and the existing ownership check.claude-profile-pointers/account-<id>instead of the selection pointer. Orca never deletes these files, so closing or moving tabs cannot unpin an agent. Removing an account first points its file at an empty placeholder folder, so aclaudetyped again in that terminal reaches Claude's own sign-in, never another account's login. Theclaudeshell function and the selection pointer's writer are unchanged.agent-kept-conversations.json): explicit keeps only, no cap, no expiry, and the file does not exist until the first keep. A keep follows a session-id change inside the same Claude process (/clear,/resume). An unreadable file is reported as unavailable rather than empty, a file written by a newer Orca is never rewritten, and a corrupt one is set aside, not deleted.CODEX_HOMEis set (it would need a second copy of the login), a kept Codex transcript that cannot be found, and a lane mismatch. Refusing a kept resume never traps the agent: un-keeping needs no terminal.Not in this PR: WSL panes (no account shown, Keep not offered, "Keep old account" as today), SSH relay panes and remote runtimes, native chat sessions (chats publish no account and are not counted), a default account per workspace, an
--accountoption on CLI/RPC launches, placing new agents by usage, and moving idle agents.Why
Orca has one selected Claude account and one selected Codex account per runtime. Every new agent starts on it, and switching moves the next start of every agent at once. People with more than one subscription want some agents to stay where they are while others move.
Where this goes (not part of this PR). Step 2 is opt-in and off by default: an agent start that is not kept picks the account on its lane with the longest runway before a configurable line (default 90%), using only readings Orca trusts; an account at its line takes no new starts, and an account Orca cannot read is never a target. Step 3 is also opt-in: when an account reaches its line, agents on it that have finished their turn and sat idle (cache cold) get a one-click "Continue on <account>", with a separate setting to do it automatically; an agent that is generating, waiting for an answer or permission, or kept is never moved. The pieces left in this PR for those steps: the
keptmode and the account facet, one resolver both spawn paths already call, the per-account Claude pointer, and the account-parameterized Codex home resolution for resume.Linked Issue
Fixes #28683
Related: #9496 (this is its per-session half; a per-workspace default can build on it) and #20512 (this is the first step toward agent-level account scheduling).
Visual Proof
Before is the base commit's build, after is this branch, same scenario, light and dark. Both come from the repo's Playwright + Electron e2e harness under Xvfb, with placeholder accounts (
alice@example.com,bob@example.com) and no real login. No realclaudeorcodexran: agent status rows were driven by posting hook events to the app's local hook endpoint, and the usage percentages in the status bar are injected demo values. "Sign in to see usage" underalice@example.comis only the placeholder account having no login. In every shot the selected account isalice@example.com, except the switchers, where it has moved tobob@example.comafter two agents started on alice.Not captured as images (covered by unit tests instead): the account-removal confirmation line, the Codex "Account switched" prompt with Keep old account, and a split tab's one-line-per-pane hover card.
Testing
How a reviewer can try it by hand:
What I ran, on Linux only (x64, Node 24, headless Electron under Xvfb). I did not test on macOS or Windows, and I did not click through the app by hand: the UI was driven through the Playwright e2e harness.
Typecheck:
tc:node,tc:cli,tc:webpass.Lint: every step of
pnpm lintpasses except oneoxlintfinding in a file this PR does not touch:src/main/orcad/orcad-entry.tsis 301 lines against a limit of 300, and the file is identical at the base commit. The chain stops at its first failure, so I ran the 22 steps afteroxlintone by one and all pass (including the max-lines and layout-writer ratchets, the localization catalogs, extraction and coverage checks, and the design-system and changed-code quality gate).oxfmt --checkis clean on the 123 changed files.Unit tests: the 34 test files this PR adds or changes plus 6 existing suites that must keep passing unchanged (PTY launch parity and race parity, Codex home routing, daemon spawn auth, the Claude profile router, the shared-state-survives-a-failed-read test) are 40 files, 466 tests, all passing. A wider run over
src/main(agent-hooks, ipc, startup, claude-accounts, codex, codex-accounts, agent-accounts),src/shared,src/preloadand the renderer's tab-bar, status-bar, settings and store folders passed 30,030 tests and failed 6. Five of those fail the same way on the base commit and depend on the machine (running as root, acodexbinary onPATH); the sixth is a 30 s timeout inwindows-transient-lock-removal.test.tsunder heavy load, and that file passes on its own on this branch (6/6) and on the base.e2e: the two new specs,
agent-account-keep-claude.spec.tsandagent-account-keep-codex.spec.ts, and the existingcodex-account-switch-dismiss.spec.tspass (3/3). The Claude spec puts a stand-inclaudeonPATHthat prints which account folder and pointer file a launch reads, so the real shell function and pointer are exercised without starting Claude; it is skipped on Windows.Not run: a production
pnpm build(only an e2e-modeelectron-vite build), the other e2e specs, and the PowerShell and fishclaudefunctions beyond their existing unit tests.Not observed: a Claude or Codex resume across two real subscriptions. No test starts a real agent, and a kept agent only ever resumes on the account it ran on.
I manually tested these changes locally (driven through the e2e harness on Linux, not by hand; macOS and Windows not tested)
Automated tests added/updated, or explained why not below
AI Disclosure
Claude Opus 5.5 (Anthropic), used through Claude Code, took part in the design, the implementation and the review of this change. I reviewed the result.
Review
Where I would most like a reviewer's eye:
runAccountand is forwarded to the renderer only, so plugin listeners, the session recorder and paired clients see no change. The window listener must not treat it as a status event (a Codex pane waiting for approval would otherwise get a forced "action required" title and a bell), so the republish is flagged, and a test covers it.claudetyped by hand into that already-open terminal still follows the selection until the next Orca-made start, because that terminal's environment was fixed when it started; the hover card says "from its next start". Codex has no such gap (its home is fixed at spawn).keptinstead of deleting the record. For the prompt this is equivalent; the difference is that the keep now survives a restore.Agent skill upstream boundary
Notes
claudeshell functions (POSIX, fish, PowerShell) are unchanged. The new Claude e2e uses a POSIX probe and is skipped on Windows.keepStalePanescall; web clients never see the facet, so they offer no Keep.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)