Skip to content

[Bug]: Restarting Orca drops live SSH agent tabs and brings back stale ones: leftover local rows win over the ssh: partition at boot #23390

Closed

Description

@tlgnkl

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)

  1. Four live agent tabs came back titled Terminal 18, Terminal 20, Terminal 22 and Terminal 23 instead of their agent titles. Then they were not shown at all: not in the tab bar, not in orca terminal list. The claude processes kept running under the relay (checked with ps on the host, and ~/.claude/sessions/<pid>.json gave their session ids).
  2. Fresh empty shells were spawned in those worktrees. In main.trace.ndjson, the persistence.pty-binding spans right after that launch show 9 × binding.origin: "spawn" and 1 × "reattach" for binding.host: "ssh". The relaunch 48 minutes earlier showed 18 spawns and 2 reattaches.
  3. Before those relaunches, terminal_pane_recovery_remount with reason: "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

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 local are restored and get fresh shells (the empty zsh pile);
  • live tabs whose local row has no unifiedTabs / tabGroups entry are restored without a surface. The layout refusal then expires their lease and deliberately leaves the process running, which matches the comment in ssh-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.

  1. Check orca-data.json: workspaceSession.tabsByWorktree has non-empty rows for an SSH worktree, and workspaceSessionsByHostId['ssh:<id>'].tabsByWorktree has a different, current set for the same key.
  2. Start an agent in a new tab in that worktree.
  3. Quit Orca (the normal quit path), then relaunch.
  4. The agent tab is gone or comes back as Terminal N without a surface. Tabs that exist only in the local row come back as new shells. ps on the host still shows the agent.

Expected

  • For an SSH workspace, the ssh: partition is authoritative once it has rows, and local rows only fill gaps. Or, simpler: after the one-time fold, SSH workspace rows are deleted from local so there are no longer two copies.
  • A detached lease 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 (the terminal.adoptOrphans machinery already exists) or at least shown as an orphan in orca 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

Activity

  1. AmethystLiang commented on Sep 27, 2026

    @AmethystLiang
    Contributor

    thanks for reporting. looking into it

  2. tlgnkl commented on Sep 28, 2026

    @tlgnkl
    Author

    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.

  3. OrcaWin commented on Oct 9, 2026

    @OrcaWin
    Collaborator

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

os:macosIssues affecting macOS

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions