{"record":{"id":"82f4fffe498282d3","repo":"stablyai/orca","slug":"terminal-host-gone","errorCode":null,"errorMessage":"terminal_host_gone","messagePattern":"terminal_host_gone","errorType":"exception","errorClass":"TerminalHostGoneError","httpStatus":null,"severity":"error","filePath":"src/main/ipc/pty.ts","lineNumber":859,"sourceCode":"      expectedIncarnationId: owner.runtimeIncarnationId ?? owner.persistedIncarnationId,\n      expectedIncarnationIsAuthoritative: owner.runtimeIncarnationId !== undefined,\n      isNewSession: undefined,\n      command: undefined,\n      commandDelivery: undefined,\n      startupCommandDelivery: undefined,\n      launchAgent: undefined,\n      startupIngress: undefined,\n      agentSessionEnsure: undefined,\n      agentSessionCreateOperationId: undefined,\n      onPtySpawnCommitted: undefined\n    })\n  } catch (error) {\n    if (error instanceof TerminalSessionOwnerUnverifiedError) {\n      throw new Error('terminal_pane_owner_unverified')\n    }\n    // Why: translate before paired-runtime RPC strips the socket error's code and syscall.\n    if (isDaemonEndpointGoneError(error)) {\n      throw new TerminalHostGoneError()\n    }\n    if (!isPtyAlreadyGoneError(error)) {\n      throw error\n    }\n    const ownerBeforeRetire = args.resolveOwner?.()\n    if (\n      ownerBeforeRetire &&\n      (ownerBeforeRetire.ptyId !== owner.ptyId ||\n        ownerBeforeRetire.runtimeIncarnationId !== owner.runtimeIncarnationId ||\n        ownerBeforeRetire.hasPersistedBinding !== owner.hasPersistedBinding ||\n        ownerBeforeRetire.persistedIncarnationId !== owner.persistedIncarnationId)\n    ) {\n      throw new Error('terminal_pane_owner_changed')\n    }\n    runtime?.onPtyExit(owner.ptyId, 0, owner.incarnationId)\n    clearProviderPtyState(owner.ptyId)\n    ptyOwnership.delete(owner.ptyId)\n    if (","sourceCodeStart":841,"sourceCodeEnd":877,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/pty.ts#L841-L877","documentation":"Thrown as a TerminalHostGoneError from attachStablePaneOwner when reattaching to a stable terminal pane whose host daemon socket endpoint is provably gone. isDaemonEndpointGoneError only matches connect-syscall errors with code ENOENT or ECONNREFUSED — i.e. the daemon's published socket path no longer exists or actively refused the connection. The translation happens here because the paired-runtime RPC layer downstream strips the underlying socket error's `code` and `syscall` fields, so the signal would be lost if not converted now.","triggerScenarios":"Calling provider.spawn with attachOnly:true on a stable pane owner after the daemon process exited, was upgraded, or its canonical socket path was renamed away. Reproduced when a worktree's terminal pane tries to reattach on app restart or window re-creation but the daemon that hosted the original PTY is no longer bound.","commonSituations":"Daemon crash or restart between sessions; daemon upgrade mid-flight; socket path replaced by a fresh publisher (per the daemon-endpoint-ownership protocol); SSH host disconnected after the pane owner was persisted; user killed the daemon process manually.","solutions":["Catch TerminalHostGoneError by class and surface a 'terminal needs to be reopened' UI; the pane cannot reattach to a dead host.","Verify daemon liveness before attempting attach (provider.listProcesses) so the gone-endpoint state is detected explicitly instead of via throw.","Restart the daemon (or trigger a fresh spawn that rebinds the canonical endpoint) and let the pane spawn fresh rather than reattach.","If recurring, inspect daemon logs for why it exited and check the canonical socket path is being republished by the endpoint-ownership protocol."],"exampleFix":"// before\ntry {\n  await attachStablePaneOwner(args)\n} catch (err) {\n  // generic handling — loses the host-gone signal\n}\n\n// after\nimport { TerminalHostGoneError } from '../daemon/daemon-errors'\ntry {\n  await attachStablePaneOwner(args)\n} catch (err) {\n  if (err instanceof TerminalHostGoneError) {\n    // pane cannot be reattached; retire the persisted binding and spawn fresh\n    retirePersistedStablePaneOwner(args.store, args.owner, args.worktreeId, args.connectionId)\n    return spawnForStablePane({ ...args, owner: null })\n  }\n  throw err\n}","handlingStrategy":"try-catch","validationCode":"import { isDaemonEndpointGoneError } from '../daemon/daemon-errors'\nimport type { IPtyProvider } from '../providers/types'\n\nasync function canReachDaemon(provider: IPtyProvider, deadlineMs = 1000): Promise<boolean> {\n  try {\n    await provider.listProcesses({ deadlineMs })\n    return true\n  } catch (err) {\n    return !isDaemonEndpointGoneError(err)\n  }\n}","typeGuard":"import { TerminalHostGoneError } from '../daemon/daemon-errors'\n\nfunction isTerminalHostGone(err: unknown): err is TerminalHostGoneError {\n  return err instanceof TerminalHostGoneError\n}","tryCatchPattern":"try {\n  await attachStablePaneOwner(args)\n} catch (err) {\n  if (err instanceof TerminalHostGoneError) {\n    retirePersistedStablePaneOwner(args.store, args.owner, args.worktreeId, args.connectionId)\n    return spawnForStablePane({ ...args, owner: null })\n  }\n  throw err\n}","preventionTips":["Before reattach, call provider.listProcesses with a short deadline to confirm the daemon endpoint is reachable.","Wrap begin/end of any daemon-replacement flow in try/finally so the socket always ends in a known state.","Never treat a missing socket file as proof the daemon is dead — connect ECONNREFUSED/ENOENT is the only proof per the endpoint-ownership protocol."],"tags":["terminal","daemon","socket","reattach","stable-pane"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}