Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

### Machine admission inventory

- `pty list --json`, remote inventory, and live status now expose the opaque
daemon `generation` alongside the stable session `name`. Current sessions
return a string; flattened historical generation-less inventory returns
`null`. Machine clients can discover the exact identity pair required by
`machine-attach-v2` without parsing registry files or substituting
`createdAt` as an unsafe heuristic.

### Storage format

- Supporting live daemons now advertise a `recovery` capability in session
Expand Down Expand Up @@ -84,18 +93,17 @@ notification because pty does not journal a cross-file transaction.
fail closed while listing the candidate stable ids. Fabric remote routing uses
the same rule on the target host.

### Framed machine attach stream

- `pty attach --attach-stream-fd-v1 <fd> <ref>` keeps stdin/stdout as the
controlling terminal while writing ordered, existing-protocol `GEOMETRY`,
`SCREEN`, and `DATA` packets plus a terminal `EXIT` or `DETACH` outcome to a
dedicated inherited descriptor. Intentional local detach is framed and
flushed even when it occurs before the initial daemon baseline, so consumers
can distinguish it from a truncated stream.
The descriptor remains caller-owned and must be closed by the caller for
consumers to observe EOF. Invalid descriptors, write failures, and daemons
that do not provide the v1 geometry-first contract fail clearly on stderr.
- Ordinary interactive attach rendering and resize behavior are unchanged.
### Headless machine attach

- `pty machine-attach-v2` is the sole machine-facing attach API. It owns stdin
and stdout as bounded, direction-specific framed streams and admits one exact
stable session id and expected daemon generation on one socket. Successful
admission emits `HELLO`, an optional atomic `READY` baseline with ordered
terminal updates, one typed terminal outcome, and EOF. Admission failures
are typed separately from failures after `HELLO`.
- The adapter never resolves display-name aliases, restarts a session, reserves
terminal input bytes, or falls back to interactive attach. `pty attach`
remains exclusively interactive.

### Stream-ordered effective geometry for embedded clients

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pty list --filter-tag role=web # show only sessions with matching tag
pty attach myserver # reconnect to a session
pty attach -r myserver # reconnect, auto-restart if exited
pty attach --no-restart myserver # attach only; fail if not running
pty attach --attach-stream-fd-v1 3 myserver 3>events.bin # framed machine stream
pty machine-attach-v2 < request.frames > response.frames # framed terminal-host API
pty exec -- codex # replace this session's process (inside a session)
pty peek myserver # print current screen and exit
pty peek --plain myserver # print as plain text (no ANSI)
Expand Down
8 changes: 3 additions & 5 deletions completions/pty.bash
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ _pty() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
commands="run attach a exec peek send events list ls stats restart kill recover rm remove gc tag tag-multi emit rename metadata up down test remote-serve"
commands="run attach a machine-attach-v2 exec peek send events list ls stats restart kill recover rm remove gc tag tag-multi emit rename metadata up down test remote-serve"

if [[ ${COMP_CWORD} -eq 1 ]]; then
if [[ "${cur}" == -* ]]; then
Expand All @@ -27,15 +27,13 @@ _pty() {
COMPREPLY=($(compgen -W "-d --detach -a --attach -e --ephemeral --id --name --no-display-name --tag --env --unset-env --cwd --isolate-env --force" -- "${cur}"))
;;
attach|a)
if [[ "${prev}" == "--attach-stream-fd-v1" ]]; then
return
fi
if [[ "${cur}" == -* ]]; then
COMPREPLY=($(compgen -W "-r --auto-restart --no-restart --force --remote --attach-stream-fd-v1" -- "${cur}"))
COMPREPLY=($(compgen -W "-r --auto-restart --no-restart --force --remote" -- "${cur}"))
else
COMPREPLY=($(compgen -W "${names}" -- "${cur}"))
fi
;;
machine-attach-v2) ;;
exec)
COMPREPLY=($(compgen -o dirnames -- "${cur}"))
;;
Expand Down
2 changes: 1 addition & 1 deletion completions/pty.fish
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ complete -c pty -n __pty_needs_command -l filter-tag -x -d 'TUI: filter to k=v (
complete -c pty -n __pty_needs_command -a run -d 'Create a session and attach'
complete -c pty -n __pty_needs_command -a attach -d 'Attach to an existing session'
complete -c pty -n __pty_needs_command -a a -d 'Attach to an existing session'
complete -c pty -n __pty_needs_command -a machine-attach-v2 -d 'Headless framed duplex attach for terminal hosts'
complete -c pty -n __pty_needs_command -a exec -d 'Replace the current session process'
complete -c pty -n __pty_needs_command -a peek -d 'Print current screen (or follow / wait-for-text)'
complete -c pty -n __pty_needs_command -a send -d 'Send text or key events'
Expand Down Expand Up @@ -82,7 +83,6 @@ complete -c pty -n '__pty_using_command attach a' -l auto-restart -s r -d 'Auto-
complete -c pty -n '__pty_using_command attach a' -l no-restart -d 'Attach only; never prompt or restart an exited session'
complete -c pty -n '__pty_using_command attach a' -l force -d 'Attach even from inside another pty'
complete -c pty -n '__pty_using_command attach a' -l remote -d 'Attach a session on a fabric peer'
complete -c pty -n '__pty_using_command attach a' -l attach-stream-fd-v1 -x -d 'Write framed machine events to an inherited fd'
complete -c pty -n '__pty_using_command attach a' -a '(__pty_sessions)' -d 'Session'
complete -c pty -n '__pty_using_command exec' -F
complete -c pty -n '__pty_using_command peek' -l follow -s f -d 'Follow output read-only'
Expand Down
4 changes: 3 additions & 1 deletion completions/pty.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ _pty() {
'run:Create a session and attach'
'attach:Attach to an existing session'
'a:Alias for attach'
'machine-attach-v2:Headless framed duplex attach for terminal hosts'
'exec:Replace the current session process'
'peek:Print current screen (or follow / wait-for-text)'
'send:Send text or key events'
Expand Down Expand Up @@ -75,9 +76,10 @@ _pty() {
'--no-restart[Attach only; never prompt or restart an exited session]' \
'--force[Attach even from inside another pty]' \
'--remote[Attach a session on a fabric peer]' \
'--attach-stream-fd-v1[Write framed machine events to an inherited fd]:fd:' \
'1:session:_pty_sessions'
;;
machine-attach-v2)
;;
exec)
_arguments \
'1:directory:_directories'
Expand Down
51 changes: 30 additions & 21 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { PacketReader, MessageType } from "@compoundingtech/pty/protocol";
List all retained sessions without mutating the registry. Cleanup is owned by
explicit lifecycle operations such as `gc()` and `cleanupAll()`.

`SessionInfo.metadata?.generation` is the current daemon's opaque admission
token. `pty list --json` projects it as `generation: string | null` alongside
the stable `name`, and remote inventory preserves the same pair. Machine
clients pass a non-null observed token as `OPEN.expectedGeneration`; `null`
identifies historical metadata that cannot be admitted through machine attach
v2. `createdAt` is presentation metadata and is never an admission token.

### `getSession(ref: string): Promise<SessionInfo | null>`

Resolve a stable session id or display name. An exact stable id always wins. A
Expand Down Expand Up @@ -148,6 +155,7 @@ interface SessionInfo {
}

interface SessionMetadata {
generation?: string; // opaque daemon-generation token
command: string;
args: string[];
displayCommand: string;
Expand Down Expand Up @@ -306,6 +314,7 @@ each writable client's requested size and which min-wins axes it constrains.
```typescript
interface StatsResult {
name: string;
generation: string; // opaque daemon-generation token
terminal: {
cols: number; rows: number;
cursorX: number; cursorY: number;
Expand Down Expand Up @@ -367,22 +376,24 @@ These functions use `process.stdin`/`process.stdout` directly and may call `proc

Interactive attach with bidirectional I/O. Takes over stdin/stdout. Ctrl+\ to detach (double-tap to send through).

Set `attachStreamFdV1` to a writable inherited descriptor (3 or greater) for
machine mode. stdin and stdout remain the controlling terminal for input and
resize events, but terminal output is written only to that descriptor using the
existing protocol framing. Version 1 emits ordered `GEOMETRY`, `SCREEN`, and
`DATA` packets followed by one terminal outcome: `EXIT` when the session process
ends or `DETACH` when the local user intentionally detaches. `DETACH` may be the
first packet when the user detaches before the daemon supplies its initial
baseline. Each initial attach or reconnect otherwise starts with `GEOMETRY`; a
daemon that sends terminal data first is rejected as unsupported.

The descriptor remains caller-owned. `attach()` flushes its writer but does not
close the descriptor, so a consumer sees EOF only when the caller closes its
copy (or the process exits). A clean EOF follows a framed `EXIT` or `DETACH`;
EOF without either outcome is a truncated stream. Descriptor errors fail the
attach and are reported on stderr; stderr text is never written into the framed
stream.
This API is exclusively interactive. Terminal hosts use `machineAttachV2()` or
the equivalent `pty machine-attach-v2` command instead.

### `machineAttachV2(options?: MachineAttachV2Options): Promise<MachineOutcome>`

Bridge one headless, framed stdin/stdout stream to an exact running session.
The first request is an `Open` frame containing the stable session id, expected
daemon generation, initial geometry, and required capabilities. The adapter
uses one daemon socket and never resolves aliases, restarts sessions, reserves
input bytes, or falls back to interactive attach.

After successful admission, responses are ordered as `Hello`, an optional
atomic `Ready` baseline with zero or more `Data`/`Geometry` updates, one typed
`Exited`, `Detached`, or `StreamFailure` outcome, then EOF. Rejection before
`Hello` produces `AdmissionFailure`. Request and response frames are
direction-specific; use `encodeMachineRequest`/`decodeMachineResponse` and
`MachineFrameReader` from `@compoundingtech/pty/client` rather than the
interactive packet protocol.

### `peek(options: PeekOptions): void`

Expand Down Expand Up @@ -501,11 +512,9 @@ const MessageType = {
};
```

`DETACH` always has an empty payload. On the session socket it requests that
the client connection detach. On `--attach-stream-fd-v1`, it is the terminal
outcome for an intentional local detach and is flushed before clean completion.
Clean EOF follows either `DETACH` or `EXIT`; EOF without either outcome is a
truncated stream.
`DETACH` always has an empty payload. On the interactive session socket it
requests that the client connection detach. Machine attach uses its own typed,
direction-specific request and response frames.

Packet types are length-delimited. Clients predating `GEOMETRY` ignore the
unknown bounded packet and continue with following `SCREEN`/`DATA`, preserving
Expand Down
25 changes: 18 additions & 7 deletions docs/vrs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,22 @@ implementation contract and validation map live in [spec.md](./spec.md).
- **R07 Bounded stream protocol:** Packets are length-delimited, fragmented
input is reassembled, oversized input is rejected without unbounded
buffering, and reconnect or unsupported capability failure is explicit.
- **R08 Machine attach outcomes:** Machine attach preserves the framed
`GEOMETRY`, `SCREEN`, `DATA`, and `EXIT` stream on a caller-owned inherited
descriptor while stdin/stdout remain the controlling terminal. A clean stream
ends with exactly one `EXIT` when the session process ended or empty `DETACH`
when this client intentionally detached; EOF without either is truncation.
Administrative destruction is truncation unless process `EXIT` was observed.
Machine admission verifies the exact stable id and generation on the same
connection it attaches; rejection causes no attach, resize, or redraw
mutation. Capability support is negotiated behaviorally rather than inferred
from version strings, record shapes, separate preflight observations, or
timeouts.
- **R08 Machine attach stream:** Machine attach is an exclusively framed,
bidirectional stdin/stdout protocol distinct from interactive attach. One
`OPEN` receives either exactly one `ADMISSION_FAILURE` then EOF, or `HELLO`,
an optional atomic `READY` baseline with ordered updates, exactly one typed
bound-stream outcome, then EOF. `READY` contains effective
geometry, the complete revisioned child-input mode snapshot, and screen
bytes. `INPUT` framing preserves its byte payload; the child boundary accepts
valid UTF-8 plus all C0 and escape bytes without replacement or reserved-byte
interpretation and rejects invalid UTF-8 explicitly. Detach is its own frame,
never inferred from input. EOF before a typed outcome is truncation, and
administrative destruction is truncation unless process exit was observed.

### Must expose durable state through one behavioral core

Expand All @@ -99,4 +109,5 @@ implementation contract and validation map live in [spec.md](./spec.md).
protocol/testing APIs, the shipped package entrypoint, local transport, and
remote routing preserve the applicable runtime, stream, geometry, registry,
and lifecycle contracts. A surface rejects unsupported capabilities instead
of silently weakening them; tests use real PTYs and processes.
of silently weakening them; machine attach never falls back to an older
protocol after v2 admission fails. Tests use real PTYs and processes.
Loading
Loading