Pre-1.0. Format may change in any release; pin to a pty version if you depend on these files. Breaking changes appear under
### Storage formatin the CHANGELOG.
For non-Node tools that want to read pty's state without paying Node startup. The CLI is the canonical writer; the files below are the canonical readable surface.
$PTY_ROOT (default ~/.local/state/pty, mode 0700, single-user). Every CLI command honors the env var. The pre-Phase-2 name $PTY_SESSION_DIR still works with a one-time deprecation notice; set PTY_ROOT_LEGACY_SILENT=1 to suppress it.
| file | purpose | tier |
|---|---|---|
<name>.json |
session metadata | 1 |
<name>.events.jsonl |
append-only event log | 1 |
<name>.sock |
daemon IPC socket (Unix) | 2 |
<name>.pid |
daemon pid (decimal) | 2 |
<name>.lock |
creation-race lock | 2 |
.recovery/ |
authenticated request/result exchange for supported live daemons | 2 |
<name>.events.lock |
event append/retention lock | 2 |
theme |
last-selected TUI theme | 2 |
gc.log |
stdout/stderr of pty gc when run by launchd/cron (only present after auto-running gc is installed) |
2 |
<name>.json.tmp.<pid>.<rand> |
atomic-write tmp — readers MUST ignore | n/a |
<name>.events.jsonl.tmp.<pid>.<rand> |
same | n/a |
Tier 1: we'll try not to break these; changes called out in CHANGELOG. Tier 2: pty-internal; may move freely.
pty writes to <target>.tmp.<pid>.<rand> then rename()s into place. POSIX same-filesystem rename is atomic — readers see the old version or the new one, never partial. When scanning the directory, filter *.tmp.*.
Pretty-printed JSON. Source of truth: SessionMetadata in src/sessions.ts.
{
generation?: string; // opaque daemon generation; guards cleanup ownership
daemonPid?: number; // daemon owning this generation, retained after child exit
recovery?: { // signal-free live-registry recovery capability
protocol: 1;
secret: string; // opaque request-authentication key
processStartToken: string;
launchIdentity: string;
rootDevice: number;
rootInode: number;
recoveryDirDevice: number;
recoveryDirInode: number;
metadataRevision: string; // exact revision bound to retained recovery state
};
command: string; // resolved binary path
args: string[];
displayCommand: string; // command as the user typed it
cwd: string;
rows?: number; // initial terminal rows, persisted for restart
cols?: number; // initial terminal columns
ephemeral?: boolean;
isolateEnv?: boolean;
extraEnv?: { [k: string]: string }; // explicit inherited-env overlay (`--env`)
unsetEnv?: string[]; // inherited env keys removed before `extraEnv`
env?: { [k: string]: string }; // exact child env for programmatic callers
createdAt: string; // ISO 8601
exitCode?: number; // present after clean exit
exitedAt?: string;
lastLines?: string[]; // snapshotted at exit
tags?: { [k: string]: string };
displayName?: string;
lastAttachAt?: string; // ISO 8601 — set by the daemon on every non-readonly ATTACH
lastOutputAtMs?: number; // unix ms — newest PTY output observed by the daemon
}lastOutputAtMs is absent until the daemon observes output. While output
continues, a trailing-edge debounce persists the newest stamp at most once per
second; exit finalization carries the final in-memory value even when a debounce
is pending.
unsetEnv and extraEnv form the persisted inherited-environment policy.
Removals are applied first and explicit assignments second, so an assignment
wins when both mention the same key. Older metadata without unsetEnv keeps
the historical ambient-inheritance behavior.
- Status (
running/exited/vanished) is derived, not stored. A reachable socket or live pidfile process proves the daemon is running. If both paths are absent,daemonPidis accepted only when the retained recovery process-start token still matches that OS process. generationanddaemonPidare internal lifecycle guards. A daemon only removes files still owned by its generation, andpty rmwaits for that daemon to finish deferred shutdown before it reports success. Readers should treat the generation token as opaque.recoveryis present only when the daemon can prove its OS process-start identity and the selected root is owned by the daemon user with no group/other permissions. A snapshot containing this capability can authenticatepty recoverafter the socket, pid, and metadata paths are externally unlinked. The root is mode0700; treat the embedded secret as opaque and do not publish snapshots. The root and.recoverydirectory identities and permissions are revalidated before recovery state is exchanged. A signed retained revision rejects older snapshots after tags, display name, attach state, or other metadata changes. The signed revision advances before changed metadata is renamed into place: a partial publication may disable recovery, but never re-authorizes the previous snapshot. Successful recovery rotates the secret.displayNameis mutable presentation metadata and is not unique. Stable identity remains the<name>filename stem; consumers must not usedisplayNameas a durable key.- Reserved tag keys (
ptyfile*,strategy, anything starting with:) are pty/tool-internal; hidden frompty listunless--tags. - User-facing tags that drive pty behavior but are visible by default:
strategy=permanent—pty gcrespawns the session when its daemon exits (the historic supervisor's role; now stateless and run on a cron).strategy.abandon-if-cwd-gone=false— opts a permanent session OUT of the on-by-default cwd-gone reap inpty gcstep 1.5. Only meaningful withstrategy=permanent.strategy.idle-days=<N>— opts a permanent session INTO idle-reap:pty gcreaps it whenlastAttachAtis older than N days. Takes precedence over the global--idle-daysflag.parent=<name>—pty gcorphan-kills this session (SIGTERM + cleanup) when the referenced session's daemon is no longer alive. Combinator withstrategy=permanentis well-defined: orphan-kill wins.keep=true— exempts the session from reaping: unconditionally from the daemon's exit-time self-cleanup, and frompty gc's sweep until the session has been dead longer thanpty gc --keep-max-age <dur>(default 7d;0sweeps every dead keep session on that pass). Age is anchored onexitedAt, orcreatedAtfor avanishedsession that never wrote one. Its metadata,lastLines, and events file therefore survive its death until that window elapses or an explicitpty rm, whichever comes first; a running session is never swept regardless of age. Any value other thanfalse/0/no/offcounts as set, so a mis-spelled value errs toward retaining. Without this tag, a non-permanent session's files are gone the moment its command finishes.
- Lifetime: a non-permanent session's files are removed by its own daemon during shutdown once the child process terminates. Files therefore outlive the process only for
keep(bounded by--keep-max-age),strategy=permanent, externalpty kill, andvanishedsessions (SIGKILLed daemon — no cleanup code ran). Readers that poll these files after a session finishes must setkeep=trueor accept the race. - Concurrent writers: last-write-wins; readers never see torn files. Cross-process writers can lose updates to the read-modify-write window.
Append-only JSONL, one event per line. Auto-truncates from 1000 → 500 lines via atomic rewrite (the inode changes; tailing readers should re-open).
Envelope: { session: string; type: string; ts: string; ...payload }. Event types (source: src/events.ts):
| type | payload |
|---|---|
bell |
— |
title_change |
value: string |
notification |
title?, body?, source?: "osc9" | "osc99" | "osc777" |
focus_request |
— |
cursor_visible |
— |
session_start |
tags? |
session_exit |
exitCode, signal? — signal death (e.g. OOM SIGKILL) surfaces as exitCode = 128 + signal (SIGKILL 9 → 137), matching shell $?; signal carries the raw number |
session_exec |
previousCommand, command |
session_respawn |
— (pty gc respawned a strategy=permanent session) |
session_abandoned |
reason: "cwd-gone" | "idle", idleDays? — (pty gc reaped a live permanent session detected as abandoned) |
session_flapping |
counter, limit, window — (pty gc flipped a permanent session to strategy.status=flapping after N consecutive fast-fail respawns; subsequent ticks skip it) |
session_descendants_survived |
data: { pids } — a daemon signalled its child's process tree with TERM and then KILL and these processes were still alive. A record of what it could not kill, not a list of everything that outlived the session: a process that left the tree before the snapshot is not in it |
display_name_change |
previous: string|null, value: string|null |
tags_change |
previous, value (full snapshots) |
metadata_change |
previous, value containing only changed displayName and tag keys; absent tag values are null |
user.<name> |
data?, text? — free-form, via pty emit |
All event writers and retention rewrites are serialized by the per-session event lock. A complete JSONL record is therefore published without relying on an operating-system write-size limit, and retention cannot discard an append that races its atomic rewrite. Async writers wait up to five seconds for a live holder; synchronous writers fail immediately. Lock files are removed on release, and a dead holder's stale lock is reclaimed by the next writer or cleanup.
jq -r '.tags["role"] // empty' "$PTY_ROOT/myserver.json"For live updates, tail <name>.events.jsonl via inotify / kqueue. Subscribe instead of polling — metadata_change / tags_change / display_name_change / session_* fire on every mutation.