Why this API is needed
A supervisor restarting a failed PTY task has two obligations: consume the failed attempt's retained terminal evidence, then remove that same failed attempt before starting or accepting a replacement. Those steps cannot safely be composed from operations that address only the task's stable ID.
Two reconciliation passes can interleave like this:
pass 1 PTY registry pass 2
| | |
|-- observe "worker" exited ----->| generation A |
|-- read A's retained tail ------>| |
| |<-- start "worker" -------------|
| | generation B, running |
|-- remove "worker" ------------>| deletes B, not the observed A |
The stable ID still says worker, but it no longer names the generation that pass 1 observed. Evidence from A and cleanup of B have been accidentally combined. Reordering the steps is not enough: removing first can destroy A's evidence before the supervisor has durably consumed it.
PTY is the lifecycle authority that creates generations, owns the per-ID creation lock, records terminal state, and removes registry artifacts. It is therefore the only layer that can safely provide both halves of the protocol:
- A bounded, typed snapshot of one terminal generation, including its opaque generation token.
- Compare-and-remove that mutates only if the same generation is still terminal.
This must also be available as a machine CLI, not only a TypeScript function. External supervisors need the same tagged outcomes without parsing PTY's private metadata or reimplementing its locking and validation rules.
What PR #159 implements
getSessionExitEvidence(id) and pty evidence snapshot --id <id> return one tagged result. A snapshot contains the stable ID, opaque generation, exited or vanished status, nullable exit code, stream: "combined", and the exact already-persisted bounded lastLines, with an explicit unavailable-tail state.
removeSessionGeneration(id, expectedGeneration) and pty evidence remove --id <id> --expected-generation <token> remove artifacts only when the retained generation still matches and is terminal. A live or replacement generation is not removed.
- Snapshot and removal use PTY's own locks and validate retained metadata. Removal re-reads and rechecks the generation before mutation, removes metadata last, and leaves that metadata available for retry if an earlier cleanup step fails.
- The CLI emits exactly one tagged JSON document for semantic outcomes. Invalid arguments and operational failures are nonzero failures without success-shaped JSON, so non-TypeScript callers can distinguish refusal from infrastructure failure.
The evidence is intentionally bounded: this is the retained combined-stream tail PTY already owns, not a new transcript or logging subsystem.
What it intentionally does not implement
PR #159 does not choose how a supervisor publishes evidence, names or rotates log files, frames or hashes records, syncs them to durable storage, sets downstream retention, or decides when to restart. Those are consumer policy. It also does not expose live viewport state or full scrollback, make multi-file cleanup transactional, or turn a vanished session into a known exit code.
The boundary is: PTY proves which generation was observed and makes cleanup conditional on that fact; the supervisor decides what durable action must succeed before it requests cleanup.
Alternatives considered
| Alternative |
Tradeoff |
Compose list, peek, and identity-only rm |
Each call can be individually correct while the sequence races a replacement. There is no generation-bound compare step. |
| Export only the TypeScript API |
Safe for in-process TypeScript consumers, but external supervisors would still need unsupported metadata parsing or fragile command composition. |
| Give every attempt a unique stable ID |
Viable as an application convention, but it discards the stable automation address, complicates lookup and cleanup, and does not provide typed retained evidence for existing stable-ID sessions. |
| Let supervisors coordinate with their own locks |
An external lock does not serialize PTY's daemon, creation lock, or other clients. Making PTY's internal lock protocol public would expose more implementation detail than the required compare-and-remove operation. |
| Delay garbage collection or never clean up |
This narrows a timing window but does not establish correctness. It also retains stale artifacts indefinitely and leaves evidence ownership ambiguous. |
| Remove by PID or stable name |
A stable name spans generations, while PIDs can be reused and do not identify ownership of every PTY artifact. Neither is a generation capability. |
| Parse raw metadata and delete files directly |
This couples callers to private layout and schema, duplicates validation and path-safety rules, and still cannot participate safely in PTY's creation lock. |
Scope and non-goals
This issue is limited to a public exact-generation terminal-evidence snapshot and conditional cleanup on supported client and machine-CLI surfaces. It does not prescribe downstream log publication, restart/backoff, or retention policy, and it does not expand PTY's retained output beyond the existing bounded tail.
Cleanup can be partially applied if an artifact removal raises an I/O error; there is no rollback. The safety contract is that semantic refusal is non-mutating, replacement generations survive, and terminal metadata is removed last so failed cleanup remains inspectable and retryable.
Acceptance
- A real retained PTY writes unique stdout and stderr sentinels, exits nonzero, and yields a tagged snapshot whose generation, exit status, exit code, and combined tail match the persisted terminal metadata.
- If a replacement starts after that snapshot, removal using the old generation returns
generation-mismatch and the replacement remains alive and intact.
- Removal using the matching terminal generation succeeds; a matching live generation is refused without mutation.
- Missing, busy, generation-unavailable, running, invalid-metadata, exited, and vanished states remain machine-distinguishable. Malformed, oversized, symlink, and non-regular metadata fail closed as
invalid-metadata; an absent tail is not synthesized as an empty tail.
- If cleanup fails after removing an auxiliary artifact, terminal metadata and evidence remain available for an exact-generation retry.
- TypeScript and CLI surfaces expose equivalent tagged semantics; semantic CLI results are one JSON document with exit 0, while argument and operational failures emit no success JSON and exit nonzero.
Why this API is needed
A supervisor restarting a failed PTY task has two obligations: consume the failed attempt's retained terminal evidence, then remove that same failed attempt before starting or accepting a replacement. Those steps cannot safely be composed from operations that address only the task's stable ID.
Two reconciliation passes can interleave like this:
The stable ID still says
worker, but it no longer names the generation that pass 1 observed. Evidence from A and cleanup of B have been accidentally combined. Reordering the steps is not enough: removing first can destroy A's evidence before the supervisor has durably consumed it.PTY is the lifecycle authority that creates generations, owns the per-ID creation lock, records terminal state, and removes registry artifacts. It is therefore the only layer that can safely provide both halves of the protocol:
This must also be available as a machine CLI, not only a TypeScript function. External supervisors need the same tagged outcomes without parsing PTY's private metadata or reimplementing its locking and validation rules.
What PR #159 implements
getSessionExitEvidence(id)andpty evidence snapshot --id <id>return one tagged result. A snapshot contains the stable ID, opaque generation,exitedorvanishedstatus, nullable exit code,stream: "combined", and the exact already-persisted boundedlastLines, with an explicit unavailable-tail state.removeSessionGeneration(id, expectedGeneration)andpty evidence remove --id <id> --expected-generation <token>remove artifacts only when the retained generation still matches and is terminal. A live or replacement generation is not removed.The evidence is intentionally bounded: this is the retained combined-stream tail PTY already owns, not a new transcript or logging subsystem.
What it intentionally does not implement
PR #159 does not choose how a supervisor publishes evidence, names or rotates log files, frames or hashes records, syncs them to durable storage, sets downstream retention, or decides when to restart. Those are consumer policy. It also does not expose live viewport state or full scrollback, make multi-file cleanup transactional, or turn a vanished session into a known exit code.
The boundary is: PTY proves which generation was observed and makes cleanup conditional on that fact; the supervisor decides what durable action must succeed before it requests cleanup.
Alternatives considered
list,peek, and identity-onlyrmScope and non-goals
This issue is limited to a public exact-generation terminal-evidence snapshot and conditional cleanup on supported client and machine-CLI surfaces. It does not prescribe downstream log publication, restart/backoff, or retention policy, and it does not expand PTY's retained output beyond the existing bounded tail.
Cleanup can be partially applied if an artifact removal raises an I/O error; there is no rollback. The safety contract is that semantic refusal is non-mutating, replacement generations survive, and terminal metadata is removed last so failed cleanup remains inspectable and retryable.
Acceptance
generation-mismatchand the replacement remains alive and intact.invalid-metadata; an absent tail is not synthesized as an empty tail.