Files
orca/notes/orca-serve-terminal-persistence.md
2026-06-04 16:37:57 -04:00

153 lines
6.2 KiB
Markdown

# Orca Serve Terminal Persistence
## Problem
`orca serve` exposes terminal tabs to paired web clients through the runtime
`session.tabs` API, but the host-side terminal tab registry was process-local.
When the host published an empty session-tabs snapshot for a worktree, the web
client bootstrapped a new terminal, giving the user a fresh shell instead of
the previously running host session.
The browser must remain stateless for terminal identity. Browser storage is
intentionally sanitized because remote handles become stale after a new pairing
or host restart.
## Goals
- Keep the host runtime as the source of truth for paired web terminal tabs.
- Persist `orca serve` terminal tab, leaf, and PTY/session bindings in the
existing workspace session model.
- Hydrate headless `session.tabs` snapshots from host persistence before a web
client decides a worktree has no terminals.
- Mirror the SSH persistence model where it applies, while keeping SSH relay
leases and local serve persistence behind their own provider checks.
- Preserve split-pane identity by routing all activation and attachment through
parent tab id plus leaf id.
## Non-Goals
- Do not persist remote handles in browser local storage.
- Do not make browser panes supported in headless `orca serve`.
- Do not redesign the terminal daemon or SSH relay.
- Do not treat a persisted PTY id alone as proof that a live process belongs to
a pane.
## Design
### 1. Persist Runtime-Owned Serve Spawns
The runtime PTY spawn path accepts a main-only `persistHostSessionBinding` flag.
Headless serve sets it when creating session-tab terminals. The PTY handler
then calls `Store.persistPtyBinding` only after validating `worktreeId`,
`tabId`, and stable `leafId`.
This keeps unrelated renderer-local PTY spawns from writing workspace-session
terminal bindings.
### 2. Use Stable Session IDs
Serve-created terminals pass `tabId`, `leafId`, optional `sessionId`, and
`persistHostSessionBinding` into `ptyController.spawn`.
If a pending hydrated terminal has a persisted PTY/session id, activation
passes that id back to the provider. New serve-owned local sessions use a
nonnumeric `serve-${uuid}` id so they cannot collide with older numeric PTY ids
after restart.
### 3. Hydrate Headless Snapshots
Before `list`, `listAll`, subscribe initial emission, activation, close, or
move returns an empty headless state, the runtime hydrates
`mobileSessionTabsByWorktree` from `workspaceSession.tabsByWorktree` and
`terminalLayoutsByTabId`.
Hydration preserves:
- parent terminal tab id
- stable leaf id
- title fields
- active tab and active leaf
- split layout and `ptyIdsByLeafId`
- persisted PTY/session id
Legacy terminal tabs without layout entries are still hydrated using a
deterministic stable leaf id derived from the parent tab id.
### 4. Materialize Pending Tabs On Activation
Hydrated terminal surfaces with no live trusted handle are exposed as
`pending-handle`. Activating one in headless serve materializes the exact
parent tab and leaf on the server, then returns a ready terminal surface.
Explicit leaf activation is exact. If `leafId` is provided and the requested
leaf is missing, the runtime returns `tab_not_found` instead of falling back to
a sibling.
### 5. Require Trusted PTY Identity
For headless-hydrated persisted tabs, a live PTY is safe to expose only when
the runtime record already matches the same worktree, parent tab id, and pane
key. A process-list entry with the same PTY id but no pane identity stays
pending, preventing stale or numeric id collisions from attaching the wrong
terminal.
Renderer-published authoritative session snapshots retain their existing
worktree-only daemon PTY adoption path.
### 6. Close And Move Without A Renderer
Headless close and move have no-renderer mutation paths:
- close hydrates and refreshes first, removes the persisted terminal tab and
layout, updates active pointers, emits a new snapshot, and kills every live
trusted leaf under the closed parent tab
- move updates in-memory and persisted tab order without changing PTY bindings
This prevents closed or reordered tabs from reverting on reconnect.
### 7. Renderer Guards
The web client still drops remote terminal identity from browser storage. It
mirrors the host snapshot and activates pending host mirrors through
`session.tabs.activate`.
Bootstrap of a default web terminal is allowed only for a fresh empty snapshot
for the active worktree when no local terminal state already exists. Stale empty
snapshots and fresh empty snapshots racing with staged local terminals do not
create duplicates.
### 8. SSH Parity
Runtime-owned SSH spawns record remote PTY leases with target-local relay PTY
ids while workspace-session PTY bindings keep app-facing ids. Lease writes are
deferred until after binding persistence succeeds for persisted runtime-owned
spawns, so a failed binding save cannot leave durable SSH lease metadata for a
tab/leaf that was not saved.
## Edge Cases Covered
- Browser reload or WebSocket reconnect mirrors host-owned terminal tabs.
- Pending headless terminal activation reuses persisted tab and leaf identity.
- Split panes attach and activate the requested leaf only.
- Removed split leaves fail fast even when siblings remain.
- Legacy tabs without layouts hydrate instead of appearing empty.
- Stale empty snapshots do not bootstrap duplicate terminals.
- Fresh empty snapshots do not bootstrap when local terminal state already
exists.
- Numeric PTY id collisions remain pending until a safe reattach/spawn occurs.
- SSH reattach failure after binding persistence failure leaves no stale lease.
## Verification Plan
- Unit-test runtime hydration, activation, close, move, split-leaf exactness,
stale PTY id handling, and legacy no-layout tabs.
- Unit-test PTY persistence gates, local session-id reattach behavior, SSH
lease parity, and persistence failure cleanup.
- Unit-test renderer snapshot bootstrap guards and remote runtime PTY transport
pending-mirror behavior.
- Run adjacent session-tab RPC and web-runtime session tests.
- Run typecheck, lint, and `git diff --check`.
- Launch the Electron dev app from this worktree with an isolated profile and
verify CDP attachment, app identity, store availability, visible boot, and
zero console errors.