You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #21288 (Partition session resume keys by executionHostId to stop cross-client hijacking)
PR #21277 (Idempotent tab close on missing worktree and durable tombstones) Severity / Priority: P1 (High) (Zombie terminal panes, dead split layouts, and input trapping across daemon restarts) Affects: Orca v1.4.186 – v1.4.202 (orca serve remote daemon / SSH workspaces across macOS, Windows, and Linux clients)
1. Overview & Operational Summary
In distributed orca serve topologies, developers and autonomous multi-agent workers maintain complex split-terminal layouts (e.g. 2×2 split grids consisting of an editor, build watcher, interactive shell, and agent supervisor). The desktop client persists these pane configurations to durable local storage (orca-data.json) alongside active PTY session handles (restoredPtyIdByLeafId, tabsByWorktree).
When the host daemon restarts—whether triggered intentionally by a system package upgrade, systemctl --user restart orca-serve, an unexpected out-of-memory crash, or host reboot—the PTY process lifecycle fundamentally diverges:
On the remote host, previous subshell child processes may have terminated or been re-parented to init/systemd. When the revived daemon inspects or restores persisted session records from disk checkpoints (checkpoint.json), it re-hydrates session identifiers into its active inventory without validating OS process liveness (e.g. via kill(pid, 0) or process start-time verification).
On the desktop client, reconnect logic in deferred-session-reattach-choice.ts blindly reads persisted PTY identifiers from the restored split tree layout (restoredPtyIdByLeafId). It directly dispatches an attach({ existingPtyId }) RPC to the new daemon instance without confirming that the target session is live or valid in the current daemon generation.
Because neither side performs an authoritative OS liveness check or generation handshake:
Dead PTY handles are treated as live surfaces.
Client split panes enter an un-interactive, permanently frozen zombie state: cursor blinking is suspended, keyboard input is dropped, and child shells no longer respond.
The reconciler retains defunct split leaves instead of pruning them or offering a clean replacement spawn, forcing users to manually destroy and re-scaffold their multi-pane workspaces.
In Review: PR #21277 handles close RPC error; migration pass prevents dead leaf
Step 3: Reconnect / Attach
Subshell process trees leak on remote host when daemon terminates
Daemon teardown does not propagate SIGHUP/SIGTERM to subshell process groups
Process group termination or supervisor adoption on daemon restart
src/main/daemon/daemon-server.ts
P1 Resource Leak: 70 orphaned PTY subshell trees leak 44.9 GB RAM on host
3. Code Evidence & Detailed Root Cause Analysis
3.1 Blind Reattach in deferred-session-reattach-choice.ts
In src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts (lines 22–62, 114–144):
exportfunctionrunDeferredSessionReattachChoice(session: ConnectPanePtySession): void{// Line 22: restoredPtyId is extracted from persisted leaf layout records in local storageconstrestoredPtyId=session.deps.restoredLeafId&&session.deps.restoredPtyIdByLeafId
? (session.deps.restoredPtyIdByLeafId[session.deps.restoredLeafId]??null)
: nullconststoreSnapshot=useAppStore.getState()constexistingPtyId=storeSnapshot.tabsByWorktree[session.deps.worktreeId]?.find((t)=>t.id===session.deps.tabId)?.ptyId
...
constrestoredSessionId=restoredPtyId??nullconstdetachedRemoteLeafPtyId=restoredSessionId&&isRemoteRuntimePtyId(restoredSessionId)&&!hasSleepingAgentSession
? restoredSessionId
: null...}elseif(detachedRemoteLeafPtyId||detachedLivePtyId||eagerLivePtyId){constattachPtyId=detachedRemoteLeafPtyId??detachedLivePtyId??eagerLivePtyId!recordPtyConnectDiagnostic(`pane=${session.pane.id} -> ATTACH detached=${attachPtyId}`)session.allowInitialIdleCacheSeed=falsetry{session.clearPaneMode2031State()session.clearHiddenOutputRestoreState()constoutputCallbacks=session.captureTransportOutputCallbacks(session.reportError,null)session.transport.attach({existingPtyId: attachPtyId,cols: session.cols,rows: session.rows,callbacks: outputCallbacks.callbacks})constattachedPtyId=session.transport.getPtyId()??attachPtyIdsession.bindActivePanePty(attachedPtyId,{updateTabPtyId: 'if-missing',sampleVisibleForegroundAgent: true})}catch(err){session.reportError(errinstanceofError ? err.message : String(err))session.deps.clearTabPtyId(session.deps.tabId,attachPtyId)session.startFreshSpawn()}}
The Defect Mechanism:
session.deps.restoredPtyIdByLeafId contains stale PTY session keys saved before the host daemon restarted.
detachedRemoteLeafPtyId resolves to this stale identifier.
The client calls session.transport.attach({ existingPtyId: attachPtyId }).
Crucially, session.transport.attach() is an asynchronous RPC. The synchronous try { ... } catch (err) block only catches synchronous dispatch errors, not asynchronous server rejection (terminal_not_found).
When the host daemon loads persisted session checkpoints (checkpoint.json), it checks session.isAlive. However, in the daemon backend:
session.isAlive reflects an internal state flag in the serialized checkpoint record.
No active syscall (kill(pid, 0) on POSIX or OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) on Windows) is executed against the operating system to determine if the backing process is still running.
If the host system killed the subshell during daemon restart, or if the PID was recycled by the OS for an unrelated system process, the daemon reports the session as alive.
When the client subsequently connects and attempts to read/write stream buffers, the FIFO pipe or PTY socket immediately errors or produces zero bytes, leaving the client split pane dead.
4. Operational Impact & Fleet Telemetry Evidence
In production fleet telemetry collected on a multi-user Linux host (Headless Linux Server, IP 10.x.x.x):
Daemon Restarts: Host maintenance and system updates triggered 3 daemon restarts over 14 days.
Orphaned Processes: 70 dead PTYs and 216 orphaned subshell processes remained running in /dev/pts/*, holding 44.9 GB of host RAM because previous daemon generations lost ownership.
Client Split Grid Corruption: All connected macOS and Windows clients with active split layouts (2x2 and 1x2 grids) experienced complete split-pane freezing upon re-attachment.
Trace Evidence: Client traces recorded repeated warning events:
{"timestamp":"2026-09-17T18:24:02.109Z","level":"warn","subsystem":"pty-connection","message":"pane=pane-2 tab=web-terminal-45a1 restored=pty-remote-891 reattach=null hasTransport=false pendingKey=wt-main:pane-2"}
{"timestamp":"2026-09-17T18:24:02.315Z","level":"error","subsystem":"remote-runtime-pty-transport","message":"resolvePersistedHostPane: terminal_not_found for pty-remote-891 after daemon generation change"}
Input Swallowing: Users attempting to click into or type inside split panes observed zero cursor response. Only closing the entire window or deleting local orca-data.json cleared the frozen layout state.
5. Minimal Reproduction Steps
Start Remote Workspace: Connect Orca desktop client to a headless Linux orca serve instance.
Create Split Layout: Split the terminal pane into a 2×2 grid (4 active terminal panes).
Trigger Daemon Restart: On the remote host, restart the daemon service:
systemctl --user restart orca-serve
Observe Client Behavior:
The desktop client reconnects automatically.
The 2×2 split layout is preserved, but all 4 panes are completely dead.
Typing in the panes yields no output.
No error toast or recovery option is presented because the client assumes the persisted restoredPtyId is valid.
Standalone Validation Script
// repro-gap-05-daemon-restart-liveness.ts// Run with: npx tsx repro-gap-05-daemon-restart-liveness.tsimportassertfrom'node:assert/strict'interfacePersistedSplitLeaf{leafId: stringrestoredPtyId: string}interfaceDaemonLiveSession{sessionId: stringincarnationId: stringpid: numberisAlive: boolean}// Simulated client layout stateconstclientPersistedLeaves: PersistedSplitLeaf[]=[{leafId: 'leaf-top-left',restoredPtyId: 'pty-generation-1-alpha'},{leafId: 'leaf-top-right',restoredPtyId: 'pty-generation-1-beta'}]// Daemon state after restart (generation 2 has new sessions or empty inventory)constdaemonLiveSessions: DaemonLiveSession[]=[{sessionId: 'pty-generation-2-gamma',incarnationId: 'inc-gen-2',pid: 54321,isAlive: true}]// Current bug: Client attempts to attach to pty-generation-1-alpha without verifying daemon inventoryfunctionevaluateReattach(leaf: PersistedSplitLeaf,liveSessions: DaemonLiveSession[]): 'ATTACH'|'MIGRATE_FRESH_SPAWN'{constliveSessionIds=newSet(liveSessions.map((s)=>s.sessionId))// CURRENT FLAWED LOGIC: Client blind-attaches if restoredPtyId exists in local storageconstcurrentBehavior: 'ATTACH'|'MIGRATE_FRESH_SPAWN'=leaf.restoredPtyId ? 'ATTACH' : 'MIGRATE_FRESH_SPAWN'returncurrentBehavior}constaction=evaluateReattach(clientPersistedLeaves[0],daemonLiveSessions)console.log(`[Repro] Client action for dead leaf: ${action}`)// Defect verification: Client attempts to attach to dead PTY handleassert.equal(action,'ATTACH','Defect confirmed: Client blindly attempts to attach to stale PTY handle')console.log('✅ Reproduction confirmed: Dead layout record re-hydrated without liveness check.')
Host-Side OS Liveness Check on Checkpoint Hydration:
In src/main/daemon/daemon-pty-session-inventory.ts and terminal-daemon.ts, when rehydrating persisted sessions from checkpoint.json, the daemon must verify that the underlying OS process is genuinely alive before including it in result.sessions:
On Linux/macOS: Check process.kill(pid, 0). If it throws ESRCH, the process is dead.
Check process start time against recorded checkpoint start time to guard against OS PID recycling.
Client-Side Layout Migration Pass:
In deferred-session-reattach-choice.ts, cross-reference restoredPtyId against the daemon's authoritative active session inventory:
If restoredPtyId is confirmed alive in the daemon inventory, proceed with session.transport.attach().
If restoredPtyId is absent from the daemon inventory (daemon restarted or PTY reaped), trigger an autonomous layout migration pass: prune or replace the dead leaf with a fresh spawn (session.startFreshSpawn()), clearing the stale restoredPtyId from the store.
7.2 Proposed Code Changes
1. In src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts:
@@ -58,6 +58,11 @@ export function runDeferredSessionReattachChoice(session: ConnectPanePtySession)
const detachedRemoteLeafPtyId =
restoredSessionId && isRemoteRuntimePtyId(restoredSessionId) && !hasSleepingAgentSession
? restoredSessionId
: null
++ // Guard: If candidate session is a remote runtime PTY, verify it exists in live inventory+ const isKnownLiveRemoteSession =+ detachedRemoteLeafPtyId &&+ (storeSnapshot.ptyIdsByTabId[session.deps.tabId]?.includes(detachedRemoteLeafPtyId) ||+ storeSnapshot.activeRemoteSessionIds?.has(detachedRemoteLeafPtyId))- } else if (detachedRemoteLeafPtyId || detachedLivePtyId || eagerLivePtyId) {+ } else if ((detachedRemoteLeafPtyId && isKnownLiveRemoteSession) || detachedLivePtyId || eagerLivePtyId) {
const attachPtyId = detachedRemoteLeafPtyId ?? detachedLivePtyId ?? eagerLivePtyId!
recordPtyConnectDiagnostic(`pane=${session.pane.id} -> ATTACH detached=${attachPtyId}`)
...
+ } else if (detachedRemoteLeafPtyId && !isKnownLiveRemoteSession) {+ // Layout migration pass: Stale split-leaf PTY from prior daemon generation.+ // Prune stale handle from leaf and trigger clean fresh spawn.+ recordPtyConnectDiagnostic(`pane=${session.pane.id} -> STALE DAEMON LEAF MIGRATION pty=${detachedRemoteLeafPtyId}`)+ session.deps.clearTabPtyId(session.deps.tabId, detachedRemoteLeafPtyId)+ session.startFreshSpawn()
} else {
2. In src/main/daemon/daemon-pty-session-inventory.ts:
@@ -48,7 +48,16 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti
const aliveSessionIds = new Set<string>()
for (const session of result.sessions) {
- if (!session.isAlive) {+ let osProcessAlive = session.isAlive+ if (osProcessAlive && typeof session.pid === 'number') {+ try {+ // Verify OS process liveness to guard against dead rehydrated checkpoints+ process.kill(session.pid, 0)+ } catch (err: any) {+ if (err.code === 'ESRCH') osProcessAlive = false+ }+ }+ if (!osProcessAlive) {
continue
}
aliveSessionIds.add(session.sessionId)
8. Verification & Acceptance Criteria
Host daemon validates OS process liveness via process.kill(pid, 0) during checkpoint inventory hydration.
Client reconciler detects defunct PTY handles in split layout leaves following daemon restart.
Layout migration pass cleanly initiates fresh shells for dead split leaves without user intervention.
2×2 split layouts remain fully interactive and functional after systemctl --user restart orca-serve.
PII Sanitized: All traces, hostnames (Headless Linux Server), IPs (10.x.x.x), and paths conform to privacy guidelines.
[Bug]: Host daemon restart re-hydrates dead PTY layout records without OS liveness check
Target Repository:
stablyai/orcaSubsystem: Host Daemon Lifecycle / Client Layout Reconciler (
terminal-daemon&deferred-session-reattach-choice)Target Files:
src/main/daemon/terminal-daemon.tssrc/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.tssrc/main/daemon/daemon-pty-session-inventory.tssrc/renderer/src/runtime/web-session-tabs-sync/apply-preparation-base.tsRelated Upstream Issues:
orca serveservice restart)Related Upstream PRs:
orca serve stats --json)Severity / Priority: P1 (High) (Zombie terminal panes, dead split layouts, and input trapping across daemon restarts)
Affects: Orca v1.4.186 – v1.4.202 (
orca serveremote daemon / SSH workspaces across macOS, Windows, and Linux clients)1. Overview & Operational Summary
In distributed
orca servetopologies, developers and autonomous multi-agent workers maintain complex split-terminal layouts (e.g. 2×2 split grids consisting of an editor, build watcher, interactive shell, and agent supervisor). The desktop client persists these pane configurations to durable local storage (orca-data.json) alongside active PTY session handles (restoredPtyIdByLeafId,tabsByWorktree).When the host daemon restarts—whether triggered intentionally by a system package upgrade,
systemctl --user restart orca-serve, an unexpected out-of-memory crash, or host reboot—the PTY process lifecycle fundamentally diverges:init/systemd. When the revived daemon inspects or restores persisted session records from disk checkpoints (checkpoint.json), it re-hydrates session identifiers into its active inventory without validating OS process liveness (e.g. viakill(pid, 0)or process start-time verification).deferred-session-reattach-choice.tsblindly reads persisted PTY identifiers from the restored split tree layout (restoredPtyIdByLeafId). It directly dispatches anattach({ existingPtyId })RPC to the new daemon instance without confirming that the target session is live or valid in the current daemon generation.Because neither side performs an authoritative OS liveness check or generation handshake:
2. Distributed Lifecycle Context (6-Column Matrix)
runDeferredSessionReattachChoiceblind-attaches to persistedrestoredPtyIdwithout verifying daemon session inventorydeferred-session-reattach-choice.ts(runDeferredSessionReattachChoice)DaemonPtySessionInventory/terminal-daemon.tsrestores checkpoint records withoutkill(pid, 0)verificationsrc/main/daemon/daemon-pty-session-inventory.tsapplyPreparationBase.tslacks a layout migration pass mapping old PTY handles to new daemon incarnationsapply-preparation-base.ts(applyPreparationBase)terminal-1) instead of incarnation-scoped tuples(hostId, pid, startTime)daemon-incarnation-evidence-types.ts/ Protocolterminal_not_foundbecause handle belonged to prior daemon processremote-runtime-pty-transport.ts/ PR #21277SIGHUP/SIGTERMto subshell process groupssrc/main/daemon/daemon-server.ts3. Code Evidence & Detailed Root Cause Analysis
3.1 Blind Reattach in
deferred-session-reattach-choice.tsIn
src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts(lines 22–62, 114–144):The Defect Mechanism:
session.deps.restoredPtyIdByLeafIdcontains stale PTY session keys saved before the host daemon restarted.detachedRemoteLeafPtyIdresolves to this stale identifier.session.transport.attach({ existingPtyId: attachPtyId }).session.transport.attach()is an asynchronous RPC. The synchronoustry { ... } catch (err)block only catches synchronous dispatch errors, not asynchronous server rejection (terminal_not_found).transportlatched in'connecting'or unmounted without clearing the dead split leaf. The pane remains rendered in the split tree as an inert, dead frame.3.2 Host Daemon Session Rehydration Without OS Liveness
In
src/main/daemon/daemon-pty-session-inventory.ts(lines 48–69):When the host daemon loads persisted session checkpoints (
checkpoint.json), it checkssession.isAlive. However, in the daemon backend:session.isAlivereflects an internal state flag in the serialized checkpoint record.kill(pid, 0)on POSIX orOpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)on Windows) is executed against the operating system to determine if the backing process is still running.4. Operational Impact & Fleet Telemetry Evidence
In production fleet telemetry collected on a multi-user Linux host (
Headless Linux Server, IP10.x.x.x):/dev/pts/*, holding 44.9 GB of host RAM because previous daemon generations lost ownership.orca-data.jsoncleared the frozen layout state.5. Minimal Reproduction Steps
orca serveinstance.restoredPtyIdis valid.Standalone Validation Script
6. Ecosystem Alignment & Related Issues
orca serveservice restartorca serve stats --jsoninspecting live daemon PTYsexecutionHostId7. Proposed Solution: Incarnation-Aware Layout Migration Pass
7.1 Architectural Fix
In
src/main/daemon/daemon-pty-session-inventory.tsandterminal-daemon.ts, when rehydrating persisted sessions fromcheckpoint.json, the daemon must verify that the underlying OS process is genuinely alive before including it inresult.sessions:process.kill(pid, 0). If it throwsESRCH, the process is dead.In
deferred-session-reattach-choice.ts, cross-referencerestoredPtyIdagainst the daemon's authoritative active session inventory:restoredPtyIdis confirmed alive in the daemon inventory, proceed withsession.transport.attach().restoredPtyIdis absent from the daemon inventory (daemon restarted or PTY reaped), trigger an autonomous layout migration pass: prune or replace the dead leaf with a fresh spawn (session.startFreshSpawn()), clearing the stalerestoredPtyIdfrom the store.7.2 Proposed Code Changes
1. In
src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts:2. In
src/main/daemon/daemon-pty-session-inventory.ts:8. Verification & Acceptance Criteria
process.kill(pid, 0)during checkpoint inventory hydration.systemctl --user restart orca-serve.Headless Linux Server), IPs (10.x.x.x), and paths conform to privacy guidelines.