Skip to content
This repository was archived by the owner on Sep 29, 2026. It is now read-only.
Open
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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

## Unreleased

### Generation-owned startup readiness

- `spawnDaemon` and `pty run` can arm a daemon-owned startup lease. PTY
publishes one lifecycle tag containing the opaque generation, host boot
identity, and absolute monotonic deadline before detached spawn returns.
Caller exit cannot disarm or extend the deadline. Only a current-generation
live-daemon compare-and-set away from the exact starting value disarms it.
At expiry, complete frozen child containment records terminal cause
`deadline` before exact teardown. An unavailable or partial process-table
observation records `teardown-unavailable` instead, best-effort signals
observed exact identities plus the root process group, and preserves exit
evidence without claiming exact containment. Startup-lease generations retain
their terminal lifecycle and exit evidence through natural child/daemon exit
for generation-fenced consumption.
- The public client and machine CLI now expose typed accepted-socket ownership
for an exact held TCP 4-tuple and generation. Linux uses complete procfs
socket/descriptor tables; package production builds one universal Darwin
`libproc` helper for arm64 and x86_64, while source installs may fall back to
a best-effort local build without making a compiler an install requirement.
Inspection runs off the daemon event loop.
Results distinguish `Owned`, `NotOwned`, and `Unavailable`; incomplete,
scoped, or changing observations fail closed.
- `compareAndSetLifecycle` / `pty readiness cas` provide the matching
generation-fenced one-tag CAS so stale readiness or terminal completions
cannot overwrite a replacement generation.

### `pty metadata patch` waits out the attach window

- An attached child starts while `pty run` still holds the per-session
Expand Down
28 changes: 26 additions & 2 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 evidence up down test remote-serve"
commands="run attach a exec peek send events list ls stats restart kill recover rm remove gc tag tag-multi emit rename metadata readiness evidence up down test remote-serve"

if [[ ${COMP_CWORD} -eq 1 ]]; then
if [[ "${cur}" == -* ]]; then
Expand All @@ -24,7 +24,7 @@ _pty() {

case "${COMP_WORDS[1]}" in
run)
COMPREPLY=($(compgen -W "-d --detach -a --attach -e --ephemeral --id --name --no-display-name --tag --env --unset-env --cwd --isolate-env --force" -- "${cur}"))
COMPREPLY=($(compgen -W "-d --detach -a --attach -e --ephemeral --id --name --no-display-name --tag --env --unset-env --cwd --isolate-env --force --startup-timeout-ms --lifecycle-tag" -- "${cur}"))
;;
attach|a)
if [[ "${prev}" == "--attach-stream-fd-v1" ]]; then
Expand Down Expand Up @@ -128,6 +128,30 @@ _pty() {
metadata)
COMPREPLY=($(compgen -W "--id" -- "${cur}"))
;;
readiness)
if [[ ${COMP_CWORD} -eq 2 ]]; then
COMPREPLY=($(compgen -W "ownership cas" -- "${cur}"))
return
fi
case "${COMP_WORDS[2]}" in
ownership)
if [[ "${prev}" == "--id" ]]; then
return
fi
if [[ "${cur}" == -* ]]; then
COMPREPLY=($(compgen -W "--id" -- "${cur}"))
fi
;;
cas)
if [[ "${prev}" == "--id" ]]; then
return
fi
if [[ "${cur}" == -* ]]; then
COMPREPLY=($(compgen -W "--id" -- "${cur}"))
fi
;;
esac
;;
evidence)
if [[ ${COMP_CWORD} -eq 2 ]]; then
COMPREPLY=($(compgen -W "snapshot remove" -- "${cur}"))
Expand Down
7 changes: 7 additions & 0 deletions completions/pty.fish
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ complete -c pty -n __pty_needs_command -a tag-multi -d 'Bulk tag ops across sess
complete -c pty -n __pty_needs_command -a emit -d 'Publish a user.* event'
complete -c pty -n __pty_needs_command -a rename -d 'Set / show / clear displayName'
complete -c pty -n __pty_needs_command -a metadata -d 'Atomically patch presentation metadata by stable id'
complete -c pty -n __pty_needs_command -a readiness -d 'Exact socket ownership and lifecycle compare-and-set'
complete -c pty -n __pty_needs_command -a evidence -d 'Read or remove exact-generation retained exit evidence'
complete -c pty -n __pty_needs_command -a up -d 'Start sessions from pty.toml'
complete -c pty -n __pty_needs_command -a down -d 'Stop sessions from pty.toml'
Expand All @@ -89,6 +90,8 @@ complete -c pty -n '__pty_using_command run' -l unset-env -d 'Remove inherited e
complete -c pty -n '__pty_using_command run' -l cwd -d 'Working directory'
complete -c pty -n '__pty_using_command run' -l isolate-env -d 'Scrub env to a safe allow-list'
complete -c pty -n '__pty_using_command run' -l force -d 'Create even from inside another pty'
complete -c pty -n '__pty_using_command run' -l startup-timeout-ms -x -d 'Daemon-owned startup deadline in milliseconds'
complete -c pty -n '__pty_using_command run' -l lifecycle-tag -x -d 'Generation-fenced lifecycle tag key'
complete -c pty -n '__pty_using_command attach a' -l auto-restart -s r -d 'Auto-restart if the session is exited'
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'
Expand Down Expand Up @@ -154,6 +157,10 @@ complete -c pty -n '__pty_using_command rename' -l clear -d 'Remove displayName'
complete -c pty -n '__pty_using_command rename' -a '(__pty_sessions)' -d 'Session'
complete -c pty -n '__pty_using_command metadata' -l id -d 'Exact stable session id'
complete -c pty -n '__pty_using_command metadata' -x -a 'patch' -d 'Value'
complete -c pty -n '__pty_needs_subcommand readiness' -a ownership -d 'Prove accepted socket ownership for one held connection'
complete -c pty -n '__pty_using_subcommand readiness ownership' -l id -x -d 'Exact stable session id'
complete -c pty -n '__pty_needs_subcommand readiness' -a cas -d 'Compare-and-set one lifecycle tag value'
complete -c pty -n '__pty_using_subcommand readiness cas' -l id -x -d 'Exact stable session id'
complete -c pty -n '__pty_needs_subcommand evidence' -a snapshot -d 'Read retained evidence for one exact generation'
complete -c pty -n '__pty_using_subcommand evidence snapshot' -l id -x -d 'Exact stable session id'
complete -c pty -n '__pty_needs_subcommand evidence' -a remove -d 'Remove only the matching terminal generation'
Expand Down
21 changes: 20 additions & 1 deletion completions/pty.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ _pty() {
'emit:Publish a user.* event'
'rename:Set / show / clear displayName'
'metadata:Atomically patch presentation metadata by stable id'
'readiness:Exact socket ownership and lifecycle compare-and-set'
'evidence:Read or remove exact-generation retained exit evidence'
'up:Start sessions from pty.toml'
'down:Stop sessions from pty.toml'
Expand Down Expand Up @@ -68,7 +69,9 @@ _pty() {
'--unset-env[Remove inherited environment key (repeatable)]' \
'--cwd[Working directory]' \
'--isolate-env[Scrub env to a safe allow-list]' \
'--force[Create even from inside another pty]'
'--force[Create even from inside another pty]' \
'--startup-timeout-ms[Daemon-owned startup deadline in milliseconds]:milliseconds:' \
'--lifecycle-tag[Generation-fenced lifecycle tag key]:key:'
;;
attach|a)
_arguments \
Expand Down Expand Up @@ -186,6 +189,22 @@ _pty() {
'--id[Exact stable session id]' \
'1:mode:(patch)'
;;
readiness)
if (( CURRENT == 3 )); then
_values 'operation' ownership cas
else
case ${words[2]} in
ownership)
_arguments \
'--id[Exact stable session id]:id:'
;;
cas)
_arguments \
'--id[Exact stable session id]:id:'
;;
esac
fi
;;
evidence)
if (( CURRENT == 3 )); then
_values 'operation' snapshot remove
Expand Down
79 changes: 79 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,63 @@ Invalid arguments and operational failures exit nonzero with a diagnostic on
stderr. A reconciler should durably consume the snapshot before passing its
opaque generation to `remove`; a mismatch must leave the replacement intact.

### Accepted-socket ownership and lifecycle CAS

`queryAcceptedSocketOwnership(name, request)` asks the live PTY daemon to prove
that the accepted side of one exact held TCP connection belongs to the current
child process or one of its descendants:

```typescript
const ownership = await queryAcceptedSocketOwnership("dev.web", {
expectedGeneration: generation,
connection: {
localAddress: socket.localAddress!,
localPort: socket.localPort!,
remoteAddress: socket.remoteAddress!,
remotePort: socket.remotePort!,
},
});
// { _tag: "Owned", pid } | { _tag: "NotOwned" } |
// { _tag: "Unavailable", reason }
```

The caller must keep that exact connection open while querying. PTY reverses
the client-observed 4-tuple to identify the server-side `ESTABLISHED` socket,
then binds it to the exact current child identity and descendant tree. Inspection
runs in a worker so procfs, `ps`, or `libproc` latency cannot stall the daemon's
startup deadline timer. Linux uses procfs. Published packages include PTY's
universal arm64/x86_64 Darwin helper; source installs may use a best-effort
local build, and helper absence never fails package installation. Unsupported
platforms, scoped IPv6 tuples, generation mismatch, process-tree or descriptor
churn, unreadable or partial descriptor/socket tables, and helper failure return
`Unavailable`; none are
treated as `NotOwned`.

`compareAndSetLifecycle(name, request)` compares and replaces one exact tag
value through the live daemon:

```typescript
const result = await compareAndSetLifecycle("dev.web", {
expectedGeneration: generation,
tag: "run.lifecycle",
expectedValue: startingValue,
value: JSON.stringify({ _tag: "ready", generation }),
});
```

Results are tagged: `Changed`, `Unchanged`, `ValueMismatch`, `Missing`,
`GenerationMismatch`, `Busy`, `DeadlineExpired`, `Terminal`, or
`InvalidRequest`. A replacement generation cannot be changed by a stale
completion.

The shell boundary reads the same typed request as one JSON object on stdin and
emits exactly one tagged JSON result:

```sh
pty readiness ownership --id dev.web < ownership.json
pty readiness cas --id dev.web < transition.json
```

### `validateName(name: string): void`

Throws if the name is invalid. Names must match `[a-zA-Z0-9._-]`, cannot be
Expand Down Expand Up @@ -224,6 +281,10 @@ interface SpawnDaemonOptions {
extraEnv?: Record<string, string>; // explicit assignments applied last
unsetEnv?: string[]; // inherited keys removed before assignments
env?: Record<string, string>; // exact child env; mutually exclusive with the above
startupLease?: {
timeoutMs: number; // positive startup budget, converted once
lifecycleTag: string; // exact tag carrying lifecycle JSON
};
}
```

Expand All @@ -232,6 +293,24 @@ interface SpawnDaemonOptions {
`xterm-256color`; naming either key in `unsetEnv` does not suppress those
invariants. An explicit `extraEnv.TERM` value is preserved.

When `startupLease` is present, the daemon atomically publishes the lifecycle
tag as `{\"_tag\":\"starting\",\"generation\":\"…\",\"bootId\":\"…\",\"deadlineMonotonicNs\":\"…\"}`
before `spawnDaemon()` resolves. The deadline is absolute in the named host-boot
monotonic domain. Initiating caller exit cannot disarm or extend it. Only a
successful live-daemon CAS away from that exact starting value disarms the
timer. At expiry PTY first freezes and proves complete child containment. A
complete observation records terminal cause `deadline` and terminates the exact
child tree. An unavailable or partial observation instead records terminal
cause `teardown-unavailable`, then best-effort signals every exact identity it
did observe plus the root process group; it never claims exact teardown when
containment is unknown. Both outcomes retain normal exit evidence. Child exit
before readiness similarly records terminal cause `exit`. Startup-lease
generations bypass ordinary daemon exit reaping so their terminal
lifecycle and exit evidence survive child/daemon shutdown for generation-fenced
consumption. A terminal value written by a successful current-generation
CAS is retained through the resulting child and daemon shutdown rather than
replaced by `exit`.

### `resolveCommand(cmd: string): string`

Resolve a command name to an absolute path (like `which`). Throws if not found.
Expand Down
Loading
Loading