6.2 KiB
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 serveterminal tab, leaf, and PTY/session bindings in the existing workspace session model. - Hydrate headless
session.tabssnapshots 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.