# Terminal Subsystem

## Ownership

`runtime.js` owns terminal identity, PTY processes, status, ordered output, bounded scrollback, WebSocket attachments, and lifecycle routes. `shells.js` discovers executable shell families and resolves the persisted shell ID without accepting command strings or arguments. Clients own tab arrangement and choose stable terminal IDs. Electron, hosted mobile, and Capacitor clients use this same server runtime through the shared authenticated transport.

## Protocol

`/api/terminal/ws` is the only terminal data transport. It uses v3 binary JSON control frames and is opened through `openRuntimeWebSocket`, preserving direct, Electron proxy, URL-token authentication, and private-relay routing.

- `attach` registers a connection for one terminal. One socket may attach to many terminals.
- Every attach and reconnect begins with an authoritative `snapshot` containing bounded history and the current sequence.
- A current socket that closes or errors before its initial `open` invalidates only the URL-scoped auth token used for that connection before retrying. A stale failure cannot clear a newer shared token minted for terminal, dictation, or events. Hidden or offline clients wait 60 seconds and wake promptly on visibility/online recovery.
- Keepalive: clients send `{ t: 'ping', v: 3 }` every 45 seconds and arm a 10-second pong deadline. Any inbound message (including pong) clears the deadline; a missed pong deadline closes the socket and triggers standard reconnect backoff. Connected clients send an immediate liveness probe ping on system resume, online, or visibility recovery (unless a deadline is already pending), and wake pending reconnect timers immediately. Probing is skipped while the client is hidden or offline.
- `output`, `exit`, and `restarted` carry monotonically increasing per-terminal sequences. Output carries raw live bytes plus replay-safe bytes with terminal query exchanges removed.
- Attach registers before capturing the snapshot, buffers concurrent events, drops events represented by the snapshot sequence, then enters live delivery.
- `write` always includes the terminal ID; sockets never have mutable single-terminal binding state.
- `detach` removes only that attachment.
- Creation carries the active UI appearance. The PTY sets `COLORFGBG` and answers OSC 10, OSC 11, Mode 2031, and primary-device-attribute queries immediately, including queries emitted before a WebSocket attachment exists. The DA1 fallback prevents Fish from waiting ten seconds for a renderer that cannot observe or answer its startup query. Subscribed TUIs receive a Mode 2031 notification when the appearance changes.

HTTP remains the authenticated command plane for create, resize, appearance updates, restart, close, and force-kill. There is no SSE output or HTTP input compatibility path.

## PTY Lifecycle

- IDs are client-provided or generated with `randomUUID()`.
- Concurrent creates for one ID are single-flight only when working directory and shell preference match. Existing IDs cannot be reused for another working directory.
- Dimensions are bounded to 1-1000 columns and 1-500 rows; input is capped at 64 KiB.
- A client may create before its renderer has mounted. It derives an initial size from the container and font metrics (falling back to 80x24 when unavailable), then sends a resize once the renderer reports its final dimensions. This allows shell startup and renderer initialization to overlap.
- PTY children explicitly clear `NODE_CHANNEL_FD`; daemon IPC descriptors are host-private and invalid after PTY descriptor cleanup. They carry `PICHAMBER_SERVER_TERMINAL=1` so lifecycle commands can distinguish a browser terminal descended from the server from a normal SSH or local shell; startup environment snapshots never persist this marker.
- PTY children also strip AppImage `ARGV0` (and other host-private shell vars such as `ELECTRON_RUN_AS_NODE`, `BASH_ENV`, `ENV`, `BASH_XTRACEFD`). An exported `ARGV0` makes zsh rewrite argv[0] for every external command, which breaks Python venv detection and other argv[0]/$0 consumers while leaving `/proc/self/exe` correct. On Linux, PTY spawn is wrapped with `env -u ARGV0` because `bun-pty` merges the native OS environ and would otherwise reintroduce `ARGV0` after a JS-only delete.
- `GET /api/terminal/shells` reports shell IDs available on the active server using the same augmented PATH provided to spawned PTYs, plus whether each executable has a supported login-mode argument. `auto` preserves environment/platform fallback order; an explicit unavailable shell fails creation instead of silently running a different shell. Login mode is opt-in and uses only built-in arguments for known shells. Preference changes affect new sessions and explicit restarts, not running PTYs.
- PTY data and exit callbacks enter one FIFO queue. Stale callbacks from replaced processes are ignored.
- Scrollback is retained on the server and capped at 512 KiB with UTF-8-safe trimming. Device-status, device-attribute, cursor-position reply, and color-query exchanges are removed from replay history with incomplete control sequences carried across PTY chunks; live output remains byte-for-byte unchanged.
- Exited sessions remain attachable until explicit close or idle cleanup.
- Restarts are serialized per terminal. Each restart spawns and wires the replacement before terminating the old process, retaining the terminal ID.
- Close uses SIGTERM with bounded SIGKILL escalation. Force-kill, idle cleanup, and runtime shutdown terminate process groups immediately where supported. Removal explicitly sends a fatal scoped closure and evicts client projections even when a PTY backend fails to emit `onExit`; attached terminals are not considered idle.

## Security And Relay

The WebSocket path must remain in both `isUrlAuthWebSocketPath` and relay `ALLOWED_WS_PATHS`. The client must use `getRuntimeUrlResolver().websocket()` and `openRuntimeWebSocket`; direct local URLs or raw browser WebSockets break relay and URL-token authentication.

This runtime owns the WebSocket's auth and connection lifetime: the upgrade re-validates the principal (credential store or signing secret) at establishment, so an already-minted URL token minted before a revocation or global sign-out is denied at open; the coordinator generation is captured before the auth/origin awaits and passed at track time, so a revocation racing verification reject-closes without trusting the principal and `handleUpgrade` is skipped for inactive entries. After the upgrade the connection is registered with the live-revocation coordinator under the authenticated principal, so a later revocation closes the socket within the documented latency guarantee. The 60-second URL token TTL gates establishment only — it never schedules a connection's death. Resource bounds (50 per principal / 1000 global) reject-close with `close('over-limit')` without evicting unrelated sockets. See `../client-auth/DOCUMENTATION.md`.

## Verification

Run:

```sh
bun test packages/web/server/lib/terminal/runtime.test.js packages/web/server/lib/terminal/terminal-ws-protocol.test.js
bun test packages/web/server/lib/ui-auth/ui-auth.test.js packages/web/server/lib/relay/cross-compat.test.js
```
