Repository navigation
[Bug]: Closed SSH terminal tabs resurrect on every app launch from stale legacy workspaceSession.tabsByWorktree, which host-scoped close never prunes #22503
Description
Operating system
Linux
Orca version
1.4.188
Details
Operating system
Linux (Fedora 43, x86_64). Desktop client connected to a remote Linux host over SSH (Orca relay).
Orca version
1.4.188
Details
Summary
On every launch of Orca, terminal tabs for SSH worktrees come back even though I closed them days or weeks earlier. Most of them come back as Claude agent tabs, and Orca resumes their old sessions.
The source is the legacy local workspaceSession in orca-data.json. It still has stale tabsByWorktree entries for SSH worktrees. When I close a tab, Orca removes it from the host-scoped session (workspaceSessionsByHostId[<ssh host>]) but not from the legacy local record. On the next startup the legacy tabs are merged back into the host-scoped session, and Orca spawns a fresh PTY for each one.
This looks related to #12447, but it is a different path. In #12447 a failed pty.kill leaves a live remote PTY, and reconnecting reattaches it. Here every previous PTY for the resurrected tabs was already terminated or expired. Orca rebuilds the tab from the persisted legacy record and spawns a new PTY.
Evidence (from ~/.config/orca/profiles/local-default/orca-data.json)
For one SSH worktree (<repoId>::/home/<user>/<repo>):
-
workspaceSession.tabsByWorktree[<wt>](legacy, local) had 11 tabs. They were created between Aug 24 and Sep 16, and all hadptyId: null. Most hadlaunchAgent: "claude"and anaiVaultTitleof the form{ agent: "claude", sessionId, title }. -
workspaceSessionsByHostId["ssh:<hostId>"].tabsByWorktree[<wt>]had 18 tabs. It contained all 11 legacy tab ids, each with a newptyId, plus the 7 tabs I actually use. -
The
createdAtof the re-created host-scoped tabs matched my app launch times exactly (Sep 21 14:22, Sep 21 16:53, Sep 22 20:33). -
sshRemotePtyLeasesfor those tab ids shows the pattern: each tab has 3 to 13 leases over time. Every older lease isterminatedorexpired, and each launch adds a new lease with a new PTY. Example for one tab:09-14 14:28 pty-564 terminated 09-16 23:24 pty-630 terminated 09-18 01:07 pty-707 expired 09-21 14:22 pty-738 detached <- new PTY spawned on app launchSo no old PTY was still alive to reattach. The tab was rebuilt from the persisted record.
-
22 SSH worktrees across 2 SSH repos had leftover legacy
tabsByWorktreeentries (36 tabs in total). Local, non-SSH worktrees did not. -
sshRemotePtyLeaseshad grown to 738 entries (638terminated, 66expired).
How to reproduce
I have not reduced this to a minimal repro. Based on the persisted state, the sequence looks like this:
- Have an SSH worktree whose tabs were first saved in the legacy local
workspaceSession. That was possible before the host-scopedworkspaceSessionsByHostIdexisted, or before the worktree was migrated to it. - Open terminal or agent tabs there, then close them from the tab bar.
- Quit Orca and launch it again.
- The closed tabs reappear. Agent tabs auto-resume their old sessions.
- Closing them again only fixes the host-scoped session. They come back on the next launch.
Expected
- Closing a tab in an SSH worktree removes it from every persisted session source, including the legacy
workspaceSession.tabsByWorktree,unifiedTabs,tabGroups[].tabOrder,terminalLayoutsByTabId,remoteSessionIdsByTabIdandsleepingAgentSessionsByPaneKey. - Alternatively: once a worktree has a host-scoped session, the startup merge treats that session as the source of truth and does not re-import legacy tabs for it. A migration could also drop the legacy entries for SSH worktrees once and for all.
- The startup restore should check close tombstones before recreating a tab and spawning a PTY for it.
Workaround
With Orca fully quit, I removed the SSH worktrees' terminal tabs from the legacy workspaceSession. I only touched these keys: tabsByWorktree, the terminal entries in unifiedTabs, tabGroups[].tabOrder/activeTabId, terminalLayoutsByTabId, remoteSessionIdsByTabId, sleepingAgentSessionsByPaneKey and terminalPtyIncarnationsByPaneKey. I left workspaceSessionsByHostId untouched.
After relaunching, I closed the ghost tabs one last time. Orca rewrote the file, the legacy entries stayed empty, and the host-scoped session kept only the tabs I had left open. On the next quit and relaunch, none of the closed tabs came back. That confirms the legacy local record was what re-created them.
Activity
AmethystLiang commentedon Sep 23, 2026
thanks for reporting. looking into it
tlgnkl commentedon Sep 27, 2026
Same bug on macOS → macOS SSH host, 1.4.212. Code references are checked against
main@27b823f(1.4.214). Below are the code path, a second channel that the original report doesn't cover, and why close tombstones can't stop it.The rule that makes the stale copy win
adoptStrandedHostPartitionSessionremoves a workspace fromadoptablewhenever the base (local) holds at least one terminal tab for it. The comment at L46-L49 calls that copy "the live copy the user is looking at". For a profile with leftoverlocalrows it is the stale one. The wholessh:<target>row set for that workspace is then ignored: tabs,unifiedTabs,tabGroups, andclosedTerminalTabTombstonesByTabId.This function is called from two places:
workspace-session-host-hydration.ts#L222-L243. This is the path the original report describes.remoteWorkspace:setForConnectedTargetswithout asession(use-app-session-persistence.ts#L156-L160). So main builds it from disk throughpersistedSessionForTarget, which runs the same adoption withlocalas the base (remote-workspace.ts#L238-L242). The result is sent as areplace-session(relay/workspace-session-handler.ts#L136-L139).So the stale
localcopy isn't only restored at boot. It is also written into the host snapshot (~/.orca/sessions/<ns>.json) on every sync. That explains two things. Editing the host snapshot by hand is undone within seconds. And the ghosts can come back after a plain relay reconnect, with no app restart: on one night about 46 empty shells were spawned after a reconnect.Evidence: the per-worktree snapshot is exactly the
localcopy whereverlocalis non-emptyRead-only comparison. Orca was running, and the host snapshot was at revision 14384, written a minute earlier:
localtabsssh:tabslocaltabs tombstoned inssh:localandssh:share no tab ids for SSH worktrees. All 62localrows haveptyId: null.localrows are tombstoned in thessh:partition. I closed them on Sep 26 withorca terminal close --tab, after they had resurrected and filled the relay's 50-PTY cap. They are still inlocal, and still in the host snapshot.orca terminal listshows as live (10 remote terminals, relayptys=5).localrows are not only pre-migration leftovers. TheircreatedAtvalues run from Sep 9 to Sep 26, with 11 on Sep 22 and 14 on Sep 23 (both days with an app restart or relay reconnect). None of them ever reached thessh:partition. Something in 1.4.207–1.4.212 still writes new SSH-worktree tabs intolocal. I could not pin down which write does it. A candidate: boot setscontestedPrimaryHostBySessionKey[key] = 'local'for every key the base kept (extractContestedHostSessionEntriesrecords the uncontested single-partition case too, and adoption only overrides adopted ids). The write routing then honours thatlocalowner until the SSH catalog claims the worktree (workspace-session-host-persistence.ts#L175-L182). Any tab created or re-created during that window after boot or reconnect lands inlocal.Why close tombstones (#16752) can't stop it
closedTerminalTabTombstonesByTabIdissurfaceTombstoneKeyed(ownership table). Adoption copies those records only for adoptable workspaces (L367-L375). For a base-owned workspace, the renderer boots with the stale tabs and none of their tombstones.localitself has 0 tombstones.isSuppressedByCloseignores it for any id in the current store (!currentTabsById.has(tabId)). After a boot that restored the stale rows, every ghost counts as "live locally".closed-terminal-tab-tombstones.ts#L91-L101). The host keeps listing it, because main keeps uploadinglocal. So the tombstones sit there until the 30-day TTL (L28-L31), and after that nothing suppresses the 61 tabs even in a running session.Consequence: the relay PTY cap
Each resurrected row gets a fresh shell on the host. On Sep 26 that was 45 empty
zshplus 5 agents = 50, and new terminals failed withMaximum number of PTY sessions reached (50)(relay/pty-handler.ts#L1627-L1629). Killing the shells on the host doesn't help: Orca respawns them within seconds while the tab exists. The only recovery that doesn't lose running agents is closing the tabs one by one (orca terminal close --tab). The rows stay inlocal, so the next restart brings back all 62, which is more than the cap.Suggested direction
ssh:<target>partition has rows, treat it as authoritative.localrows should only fill gaps (for example, never a tab id thessh:side has tombstoned). Or do what the partition-owner comment says will happen (workspace-session-partition-owner.ts#L16-L19) and delete SSH-worktree rows fromlocalafter the one-time fold, so the second copy stops existing.persistedSessionForTargetfrom thessh:partition, never fromlocal. The renderer could also pass its in-memory session so that main publishes what the user sees.The same adoption rule also loses live agent tabs on restart while their processes keep running on the host. I filed that side as #23390.
Happy to test a build against this profile. It reproduces deterministically for me.
OrcaWin commentedon Oct 9, 2026
Thanks for the detailed write-up, it made this much easier to track down. This should be fixed by #26098, now on main: at startup, a workspace that lives on an SSH host now takes that host's session as the source of truth, so leftover local tab rows are dropped instead of being merged back in, and they're no longer uploaded to the host either. #26088 also stops relay reattach from leaving new local leftovers behind. If closed SSH tabs still come back on a build that includes it, please reopen with details and we'll take another look.