mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
153 lines
6.2 KiB
Markdown
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.
|