Repository navigation
[Bug]: Restarting Orca drops live SSH agent tabs and brings back stale ones: leftover local rows win over the ssh: partition at boot #23390
Description
Operating system
macOS
Orca version
Incident on 1.4.207. The persisted state that causes it is still present and growing on 1.4.212. Code references are checked against main @ 27b823f (1.4.214).
Details
Short summary
On a plain relaunch (no update), live agent terminals on an SSH host lose their tabs, and tabs that were closed long ago come back as fresh empty shells. The agent processes keep running on the host.
The cause is in boot hydration. After #19572, an SSH workspace's live session lives in workspaceSessionsByHostId['ssh:<targetId>']. But the legacy copy in the local partition (workspaceSession) is never removed. adoptStrandedHostPartitionSession keeps the base (local) copy of a workspace unconditionally whenever that copy has at least one terminal tab. So for any SSH workspace with leftover local rows, the whole ssh: row set is ignored at boot, and the stale copy is what gets restored.
This is the same root rule as #22503 (closed SSH tabs come back), where I've added the code path and the host-snapshot upload channel. #22503 covers the ghost tabs. This report covers the more damaging side of it: live agent tabs lost on restart while their processes keep running.
Setup: macOS desktop client, direct SSH to a macOS (Apple silicon) host, detached relay (--grace-time 0), several Claude Code agents running in two repos on the host.
What happened (restart without update, 1.4.207)
- Four live agent tabs came back titled
Terminal 18,Terminal 20,Terminal 22andTerminal 23instead of their agent titles. Then they were not shown at all: not in the tab bar, not inorca terminal list. Theclaudeprocesses kept running under the relay (checked withpson the host, and~/.claude/sessions/<pid>.jsongave their session ids). - Fresh empty shells were spawned in those worktrees. In
main.trace.ndjson, thepersistence.pty-bindingspans right after that launch show 9 ×binding.origin: "spawn"and 1 ×"reattach"forbinding.host: "ssh". The relaunch 48 minutes earlier showed 18 spawns and 2 reattaches. - Before those relaunches,
terminal_pane_recovery_remountwithreason: "reattach-unverifiable"fired for 4–5 tabs at a time.
Over the following days the same leftover rows kept coming back after relay reconnects. Eventually they filled the relay's 50-PTY cap (Maximum number of PTY sessions reached (50), same end state as #9819). That part is covered in #22503 and #12447. This report is about the boot-time choice between the two copies.
Evidence: orca-data.json as written at quit
I have the file Orca wrote on quit, copied a few seconds after the process exited, before the relaunch. The four agent tabs exist in both partitions, with the same tab ids and the same relay ptyId:
workspaceSession (local) |
workspaceSessionsByHostId['ssh:…'] |
|
|---|---|---|
| tab rows, repo A / repo B | 20 / 23 | 10 / 11 |
the 4 agent tabs in tabsByWorktree |
yes, titles Terminal N (their creation-time titles) |
yes, real agent titles |
the 4 agent tabs in unifiedTabs / tabGroups |
no | yes |
terminalLayoutsByTabId[…].ptyIdsByLeafId |
same relay ptyId |
same relay ptyId |
sshRemotePtyLeases at quit: those four were the only leases in detached state, meaning they were cleanly detached by the quit drain, as designed (ssh-shutdown-drain.ts → detachAndPersist). After the relaunch, every lease of that relay instance is expired. The quit path left them reattachable, and boot threw that away.
The local copy is still growing on 1.4.212. Today the same profile has 30 / 31 local rows for those two repos, against 16 / 17 in the ssh: partition. So the next relaunch will do the same thing again.
Why: the adoption rule
workspace-session-host-hydration.ts#L222-L231: for eachssh:partition, boot callsadoptStrandedHostPartitionSession(session, slice, …), wheresessionis the local/merged session.workspace-session-stranded-partition-adoption.ts#L46-L49and#L84-L113: any workspace for which the base holds terminal tabs is removed fromadoptable. The comment reads: "That is the live copy the user is looking at … at the cost of not recovering rows stranded beside a populated workspace — which are stranded on main today too, so it is never a new loss."
That premise holds for the empty-row case #12721 described. It is backwards for a profile that ran builds before #19572 and kept writing local rows for SSH worktrees. There, local is the stale copy: it holds rows closed weeks ago and never removed, plus rows for newer tabs with their original title and no group membership. The live copy is the ssh: partition. With the rule as written:
- closed tabs that survive only in
localare restored and get fresh shells (the empty zsh pile); - live tabs whose
localrow has nounifiedTabs/tabGroupsentry are restored without a surface. The layout refusal then expires their lease and deliberately leaves the process running, which matches the comment inssh-orphan-relay-pty-sweep.ts#L34-L41. The agent keeps running on the host with no tab and no route back.
How to reproduce
This depends on a profile that already has SSH worktree rows in workspaceSession from before #19572. Any long-lived SSH profile does.
- Check
orca-data.json:workspaceSession.tabsByWorktreehas non-empty rows for an SSH worktree, andworkspaceSessionsByHostId['ssh:<id>'].tabsByWorktreehas a different, current set for the same key. - Start an agent in a new tab in that worktree.
- Quit Orca (the normal quit path), then relaunch.
- The agent tab is gone or comes back as
Terminal Nwithout a surface. Tabs that exist only in thelocalrow come back as new shells.pson the host still shows the agent.
Expected
- For an SSH workspace, the
ssh:partition is authoritative once it has rows, andlocalrows only fill gaps. Or, simpler: after the one-time fold, SSH workspace rows are deleted fromlocalso there are no longer two copies. - A
detachedlease whose PTY the relay still lists as alive is never expired just because its tab has no surface at boot. It should be re-surfaced (theterminal.adoptOrphansmachinery already exists) or at least shown as an orphan inorca terminal list.
Workaround
None safe. Editing workspaceSession by hand requires quitting Orca, and quitting is what triggers the problem. So today the only mitigation is not restarting Orca while agents run on the SSH host.
Related
- [Bug]: Closed SSH terminal tabs resurrect on every app launch from stale legacy
workspaceSession.tabsByWorktree, which host-scoped close never prunes #22503: same adoption rule, ghost-tab side (my comment there has the upload path and why tombstones don't help). - SSH host session partitions are written but never read — tab lists round-trip as empty and remote sync permanently deletes them #12721 / fix(session): give an SSH workspace one owning partition so its tabs stop round-tripping as deletions #19572: the partition split that created the two copies. This report is about what happens to profiles that already have both copies.
- [Bug] SSH worktree cards die after every app restart (data loss, 100% repro) #18173, [Bug]: CLI-created terminals lose their tab across a desktop restart, and inventory reports them identical to healthy ones #18191: the same "agent survives on the host, tab is lost across restart" outcome.
- SSH relay leaks orphaned PTY sessions until "Maximum number of PTY sessions reached (50)" #9819: where the resurrected shells end up (the relay's 50-PTY cap).
- Side note:
sshRemotePtyLeaseshas 116 of 127 entriesexpiredwith no pruning, spread over 11 relay instances.
Activity
AmethystLiang commentedon Sep 27, 2026
thanks for reporting. looking into it
tlgnkl commentedon Sep 28, 2026
Thanks! In case it saves time: #23394 is a ready fix for this (and for the legacy-copy upload path from #22503), with tests. Happy to rework it if you'd prefer a different approach, or close it if you're fixing this another way.
OrcaWin commentedon Oct 9, 2026
Thanks for tracking this down so thoroughly. This is fixed by #26098, now on main: for a workspace on an SSH host, the host's own session copy now wins at startup over leftover local rows, so live agent tabs and their resume records are kept and closed tabs stay closed. #26088 also stops relay reattach from writing new local leftovers. If you still lose live SSH tabs on restart with a build that includes it, please reopen with details.