This document specifies the current pty system. It builds on
requirements.md.
Draft until every mapped contract is present on the default branch. The test matrix below is the executable validation boundary.
This specification defines persistent session execution, ordered terminal transport, registry state, and supported CLI/package surfaces. It does not define a shell, window manager, multi-tenant authorization boundary, or product orchestration policy.
CLI / package / testing / remote surfaces
|
+---------+---------+
| |
ordered client stream durable registry
| |
+---------+---------+
|
per-session runtime
|
child process + PTY
The runtime owns the child process and headless terminal model. The stream projects one ordered terminal to ephemeral clients. The registry owns stable identity and durable observations. Every public surface composes these three sources rather than defining alternate semantics (R01, R09, R11).
A session is a detached daemon containing one child PTY and one headless terminal emulator. The daemon owns the socket and survives client disconnects (R01).
Launch environment assembly is ordered (R02):
replacement mode: copy env
inherited mode: process.env -> remove internal server config
isolated mode: allowlisted process.env + LC_*
policy modes only: base -> unsetEnv[] -> extraEnv{}
all modes: -> force PTY_SESSION + generation token
-> TERM absent/empty ? xterm-256color : preserve value
Replacement mode and the inherited/isolate policy options are mutually
exclusive. Metadata persists the selected mode and its removals/assignments.
Explicit restart reuses it. Permanent reconciliation re-reads a current
manifest declaration when available and otherwise uses persisted metadata.
Metadata predating unsetEnv retains historical ambient inheritance.
On child exit, the runtime drains accepted output, records final screen and exit state, emits the lifecycle event, and applies cleanup policy. Every mutating cleanup/restart path compares stable id plus generation so stale work cannot change a replacement session (R03).
Packets use a five-byte header followed by a bounded payload:
[type: uint8][length: uint32BE][payload: length bytes]
The reader reassembles partial input and rejects declared payloads above 32 MiB (R07). Unknown bounded message types are ignored for additive compatibility; capability-specific surfaces fail closed when required packets are absent.
For each admitted attach/peek generation (R04):
parser bytes before cut | parser bytes after cut | process exit
| | |
v v v
GEOMETRY -> SCREEN -----------> queued DATA -------> EXIT
The screen callback is the causal cut: SCREEN represents all earlier parser
writes; later data and exit queue behind it. A newer valid mode request
invalidates the unfinished generation. Reconnect creates a fresh generation.
A local machine detach may end with DETACH before a baseline is emitted.
Each socket begins in the command role. Role frames replace, rather than accumulate, socket state (R05):
| Frame | Resulting role | Geometry membership | Input/resize |
|---|---|---|---|
complete ATTACH(rows, cols) |
writable-attached | requested rows/cols | enabled |
recognized PEEK(flags) |
readonly | none | disabled |
malformed ATTACH |
unchanged | unchanged | unchanged |
STATUS |
unchanged | unchanged | unchanged |
Client-to-server data, status, and resize behavior is role-specific:
| Role | DATA |
STATUS |
RESIZE |
|---|---|---|---|
| command | accepted | accepted | ignored |
| writable-attached | accepted | accepted | accepted |
| readonly | ignored | accepted | ignored |
Command sockets do not receive a screen baseline and do not participate in
geometry. They may receive baseline-less live DATA or EXIT broadcasts;
those packets do not constitute reconstructable terminal state. A consumer that
needs reconstructable terminal state must first send ATTACH or PEEK. Public
stats omit command sockets and expose the writable-attached role with the
compatibility string "writable".
For writable-attached request set W, shared geometry is (R06):
rows = min(client.rows for client in W)
cols = min(client.cols for client in W)
The dimensions are minimized independently. A changed GEOMETRY notification
precedes terminal output produced after the corresponding PTY resize. Removing
the last writable-attached client leaves the last effective geometry stable.
attach --attach-stream-fd-v1 <fd> <ref> requires an inherited writable
descriptor fd >= 3. The packaged CLI runs without a wrapper child so the
descriptor, controlling terminal, signals, and process identity reach the
adapter unchanged (R08, R11).
The adapter reframes only GEOMETRY, SCREEN, DATA, and terminal outcomes to
the descriptor; terminal interaction stays on stdin/stdout and diagnostics use
stderr. It flushes exactly one clean outcome before EOF:
| Outcome | Meaning |
|---|---|
EXIT(code) |
the session process ended |
empty DETACH |
this local client intentionally detached |
| EOF without either | transport loss, reconnect give-up, descriptor failure, or abrupt administrative destruction |
The last row is a non-zero truncation, not a third clean outcome (R07, R08).
PTY_ROOT selects one registry. A stable id owns socket, metadata, events, and
generation locks; display names and tags remain mutable lookup/presentation
fields (R09).
Inventory is observational: it derives running/exited/vanished state and enriches it with live status when available, but does not restart, reap, or attach. Status reports client roles, requested/effective geometry, process resources, and terminal modes.
Metadata and events form two compatibility tiers (R10):
| Record | Contract |
|---|---|
| metadata JSON | durable launch/lifecycle source; atomic generation-aware updates preserve unknown fields |
| event JSONL | externally readable observation stream; serialized append and bounded retention |
| socket packets | internal bounded protocol with documented legacy decoding fallbacks |
Explicit lifecycle commands and gc own mutation. Cleanup is authorized by the
observed generation; removal wins over late daemon finalization, and permanent
respawn cannot overwrite a replacement (R03, R10).
A supporting daemon may publish an opaque recovery capability only when it can
prove its process-start identity and both PTY_ROOT and .recovery are private
directories owned by the daemon user. If an external cleanup unlinks that live
session's socket, pid, and metadata paths, recover --snapshot authenticates a
complete retained metadata snapshot and asks the original daemon to rebind its
listener. It preserves the daemon generation, child process, provider launch,
terminal state, and attached clients; it does not probe by signal, restart,
relaunch, or replace an occupied pathname (R03, R09).
The request/result exchange binds stable id, daemon pid and process-start token, generation, launch identity, root and recovery-directory device/inode identity, and the daemon's signed metadata revision. Metadata mutation advances the signed revision before publishing the replacement record. Recovery therefore fails closed after a partial publication and rejects missing, legacy, stale, replayed, tampered, wrong-root, permission-downgraded, or path-replacement state. Success republishes the socket, pid, and metadata with no-replace and owned-rollback semantics and rotates the recovery secret. An authenticated lock left by an interrupted recoverer may resume; other creation locks remain authoritative and are never displaced (R10).
The CLI, package entrypoint, exported client/server/protocol modules, testing library, and remote route call the same behavioral core (R11). Completion schemas preserve required option values. Remote streaming preserves local packet order and fails explicitly when the peer lacks a capability. The testing library drives real processes and PTYs and exposes screen, cursor, scrollback, input, resize, and multi-client geometry without mocks.
| Requirement | Owning source | Primary executable evidence |
|---|---|---|
| R01 | server, spawn | integration, exit reap, shutdown |
| R02 | server, spawn, sessions, ptyfile | spawn options, restart parity, restart scrub, ptyfile |
| R03 | server, sessions, recovery | kill, immediate reuse, generation guard, exit signal, recovery |
| R04 | server, connection | integration, alternate screen, scrollback |
| R05 | server | integration |
| R06 | server, protocol | effective geometry, resize, status |
| R07 | protocol, connection, remote | protocol, connection, remote reconnect |
| R08 | client, CLI, entrypoint | attach stream, signals |
| R09 | sessions, server, recovery, CLI | root, display name, status, list purity, recovery |
| R10 | sessions, events, recovery, protocol | atomic writes, metadata events, events, recovery, disk layout |
| R11 | CLI, client API, remote, testing API | help, completions, remote, screenshots, keys |
node scripts/verify-docs.ts --vrs-only validates this two-document shape,
sequential requirement IDs, links, and complete requirement references.