Venom
RFC-0010venom-spec

Durable session restore and daemon crash recovery

Field Value
Status Accepted
Accepted 2026-11-06, explicitly approved by the maintainer
Authors commixon (commixon), with Codex
Created 2026-11-06
Linear COM-243; implementation COM-244
Supersedes None
Superseded by None

History

  • 2026-11-10 (venom-org/venom-spec#144, venom-org/venomx#147; normative, narrow compatible change; accepted by venom-run (unattended, delegated by the maintainer) on 2026-11-10): §8’s Secret exclusion case allows one value derived from argv, RFC-0007 §5’s argv0 (the basename of argv[0], at most 255 bytes), in RFC-0007’s pane.list results, pane.created data and pane snapshot entity. argv0 is never the directory part of argv[0] (anything up to its last /), the bytes past 255, or another argv element. Those parts, the cwd and the environment stay excluded everywhere. §6’s “no raw argv in errors, events or doctor” rule gets the same one-clause exception. §4 is unchanged: checkpoints don’t keep argv0, so a restored archive pane has none, and a relaunched pane carries its new spawn’s. Durable formats are unchanged.

  • 2026-11-09 (venomx#60, venom-spec#129; normative, small compatible addition; explicitly accepted by the maintainer on 2026-11-09): durable format 3 (section 4.2). Writers emit format 3; readers accept formats 1, 2 and 3 and refuse every other value. Format 3 split nodes may carry RFC-0007 §4’s ratio, so a restored split keeps its shares. Format 1 and 2 files still read strictly under their own rules and restore as equal splits. A daemon that only reads formats 1 and 2 refuses a format-3 file as incompatible.

  • 2026-11-09 (venomx#89, venom-spec#110; explicitly accepted by the maintainer on 2026-11-09): small compatible addition to §5/§6 archive metadata, cut_rows, and doctor reporting for bounded literal hydration. §2 clarifies the existing count-based text-retention window. Durable formats 1 and 2 are unchanged.

  • 2026-11-08 (venomx#44, normative, with RFC-0007’s ratio; explicitly accepted by the maintainer on 2026-11-09): formats 1 and 2 hold no split ratio. A writer omits it and a reader rejects it as an unknown field, so a restored split is equal. Holding ratios needs a new format and its migration (section 4).

  • 2026-11-07 (COM-261, explicitly accepted by the maintainer): §4 clarifies that .plain text always comes from the primary screen, even while the alternate screen is active. This is a compatible clarification; the durable format is unchanged.

  • 2026-11-07 (COM-404; documents venomx COM-383/COM-309 as merged in 32d5d3d; normative, merged through the COM-404 PR, and on 2026-11-09 the maintainer agreed to mark it accepted): durable format 2 (section 4.1). Writers emit format 2; readers accept formats 1 and 2 and refuse every other value. Format 2 pane records may carry the RFC-0016 §8 float layer (layer, float) and the RFC-0016 §4 pane tags. A format-1 file reads as before: every pane tiled, no tags. Popups are never checkpointed. Restore keeps floats in their window’s float layer with fresh z-orders and re-resolved presets, and restores tags as saved (section 5). Formats 1 and 2 share everything else in section 4.

Summary

Define opt-in, versioned venomx checkpoints and explicit restore of session, window and split topology, retained exited panes, and optionally plain terminal text. Restore creates an inspectable archive, not running programs. Replacing an archived pane with a process requires a separate authorized request with a fresh argv and fresh grants. Each daemon start has fresh authority and stream identity. The maintainer accepted this design on 2026-11-06. COM-244 implements this extension after the COM-238 foundation and shared prerequisites.

The governing contracts are RFC-0001, RFC-0002, RFC-0003, RFC-0004, RFC-0005, RFC-0006 and RFC-0007. This adds an explicit durability mode to RFC-0007’s memory-only foundation. That foundation remains the default; this RFC does not supersede its transport or grant rules. It extends the shutdown persistence phase while preserving the process-termination deadline.

Motivation

After a daemon crash, users need their session organization and an honest view of what was last observed. A saved PID is not a process handle, and a saved shell command is not permission to execute it again. Restoring either blindly can kill an unrelated process or duplicate a deployment, payment or build. Durable state must therefore separate organization, historical evidence and new execution. COM-244 needs a contract for these boundaries before shipping; COM-238 and COM-223 are not expanded by this proposal.

Design

1. Headless workflow and limits

A human enables checkpoints through the ordinary ZON config verbs, then starts venomx serve. No startup, attach, reconnect or config reload implicitly restores a checkpoint. With an already running, empty daemon:

VENOMX_TOKEN=@owner venomx recovery list --json
VENOMX_TOKEN=@owner venomx recovery inspect --checkpoint c01234567-89ab-4cde-8fab-0123456789ab --json
VENOMX_TOKEN=@owner venomx recovery restore --checkpoint c01234567-89ab-4cde-8fab-0123456789ab --json
venomx session list --json
venomx pane capture --pane p0 --json

Commands use named flags, work with stdin closed, and require no presentation client. @owner is a human opt-in under RFC-0005; generated agent skills MUST NOT recommend it. Example checkpoint and source UUIDs are illustrative.

A restored pane is an archive: no PTY child, input queue, process binding or live command exists. It has exited=true for existing list/capture/stream consumers. Its additional recovery metadata distinguishes a known historical exit from interrupted or unknown process state. A missing exit code MUST NOT be interpreted as successful exit. Empty sessions and windows are preserved.

A user can explicitly replace one archive leaf with a new pane/process:

VENOMX_TOKEN=@owner venomx pane relaunch --pane p0 --argv '["/bin/sh"]' --ack-orphans=true --json

This is new execution, not a continuation. It uses the daemon’s current cwd, environment and executable resolution as RFC-0007 specifies. It MUST NOT load old argv, cwd, environment, stdin, shell state, credentials or grant profiles from disk. Restoring a checkpoint MUST execute and signal zero processes.

2. Config, storage scope and retention

Config remains ZON; checkpoints are data, never config or executable scripts. The default section, accepted by config check, is:

.{
    .recovery = .{
        .enabled = false,
        .interval_ms = 30000,
        .max_checkpoints = 3,
        .max_checkpoint_bytes = 16777216,
        .max_age_seconds = 604800,
        .terminal_text = .off,
        .text_max_rows = 10000,
        .text_max_bytes = 1048576,
        .text_max_age_seconds = 86400,
    },
}

terminal_text is .off or .plain. Plain text is an explicit privacy choice: application output, display names and screen text can contain secrets. Private files and expiry limit exposure; they cannot discover arbitrary application secrets or erase copies/backups. The maintainer selected opt-in terminal text for this RFC. Daemon credentials, principal registries and raw environments MUST never be checkpointed, even in .plain mode (section 4).

interval_ms is 1000–3600000; checkpoint count is 1–100; file size is 65536–67108864 bytes; maximum age is 1–2592000 seconds. Text rows are 0–100000, text bytes 0–16777216, and text age 1–2592000 seconds, no greater than checkpoint age. Zero rows or bytes disables text. Per-pane text caps count screen and history together; they do not increase the whole-file limit. The hard reader bounds in section 4 apply independently of configuration. Unknown enum values, invalid ranges and inconsistent age limits fail config check.

The fixed store is $XDG_STATE_HOME/venomx/recovery/<socket-key>/, falling back to $HOME/.local/state/venomx/recovery/<socket-key>/ when XDG_STATE_HOME is unset. XDG_STATE_HOME MUST be absolute. socket-key is lowercase SHA-256 of the UTF-8 absolute socket path, after resolving its existing parent directory per RFC-0002 socket selection. Store identity MUST match that path and the daemon UID; an alternate socket MUST NOT silently use the default socket’s store. There is no request-supplied path, cross-store import or network fetch in v1.

Every newly created recovery directory MUST be mode 0700 and files 0600, owned by the daemon UID. Readers/writers MUST use directory-relative, no-follow operations, reject symlinks/nonregular files, unexpected owners, hard links (link count other than one), and group/other permissions. They MUST validate the private recovery subtree and hold directory handles against substitution. A reader validates owner, mode, link count, size and type on the opened file handle and reads that same handle, never reopening a validated path. Readers MUST NOT chmod a suspicious preexisting path into apparent safety. No unsafe store is followed or repaired automatically. Same-UID hostile filesystem access is not a sandbox.

All fields reload live except store identity, which follows startup socket selection. A reload does not restore, launch, change source or replace topology. A side-effect-free validation and allocation/event preparation phase precedes retention cleanup; activation after successful cleanup MUST have no further fallible work. Explicit reload and watcher activation share this path. Startup applies the same retention cleanup before readiness; it MUST fail initialization if the configured text policy cannot be enforced on existing checkpoints. config.set only persists the requested config; config.check performs no cleanup. Failed partial cleanup may have deleted some files and MUST be diagnosed; retaining active policy does not recreate deleted files. Changing .plain to .off MUST remove stored terminal text by deleting all checkpoints containing it before reporting successful reload; if deletion or directory sync fails, reload fails and effective config remains unchanged. Disabling checkpoints MUST stop writes, but MUST NOT silently delete topology archives. A stricter age/count limit triggers pruning before successful reload. Pruning failure is visible through config errors and doctor, not silent success.

Expired checkpoints MUST be unavailable for restore, even before disk pruning. Prune on startup, before list/inspect/restore, after publication and on each interval; keep the newest permitted generations by section 3’s order. Do not prune the last valid retained checkpoint to make room for a failed write. Retention is by checkpoint generation, not by each pane’s last successful text capture. After max_checkpoints consecutive generations omit a pane’s text (for example, export failure or a text budget limit), count pruning can delete its last text-bearing checkpoint. This window is accepted by the count-based policy: max_checkpoints remains a hard count bound, and no per-pane text-bearing generation is exempt. Text remains in the live pane; a later successful capture can save it again, subject to its original expiry for an archive. A crash before that capture can lose the last durable text. No restore silently substitutes text from an older generation.

When disabled, list/inspect/forget of existing checkpoints remain available; checkpoint/restore require enabled=true. Explicit forget permits deletion without enabling future writes. Forget of a safely owned regular final file MAY delete corrupt, incompatible, expired or future-dated data without parsing its payload; unsafe filesystem objects MUST still be rejected.

3. Atomic write and crash boundaries

Only the daemon holding RFC-0002’s exclusive socket lock may write this store. A second live daemon for that socket MUST fail before touching checkpoints. Checkpoint capture serializes a consistent topology, pane lifecycle state and optional text at one reactor boundary. No partial mutation or half split can be serialized. Encoding/disk I/O runs against that immutable bounded view; PTY processing need not wait for an entire disk write. Only one write is in flight; a periodic tick while busy is coalesced into one later capture. A fresh empty daemon MUST NOT automatically checkpoint until its first successful product mutation, restore or explicit checkpoint request. Its shutdown likewise MUST NOT overwrite/prune recoverable state merely because it started empty. After activation, intentional removal of all resources is checkpointed normally. This prevents idle startup from evicting the archives the user is about to inspect.

A committed checkpoint is a single regular file c<uuid-v4>.json. A writer:

  1. Allocates a new random ID and increasing generation within the store. The next generation is one greater than the largest valid retained one; exhaustion of the safe JSON integer range fails without writing.
  2. Creates an exclusive 0600 .tmp-<uuid-v4> in that directory, writes the complete envelope, and fsyncs the file. It checks all write/close errors.
  3. Renames to the final previously unused name in the same directory, then fsyncs the directory. Only then may it acknowledge durable success.
  4. Prunes expired/excess older files and fsyncs the directory after deletions. A prune failure does not undo a published checkpoint; doctor reports it.

No mutable latest pointer is needed. Readers enumerate final filenames, validate them, and order by generation (UUID lexical order breaks ties). They MUST reject duplicate generations for restore rather than guess which was intended. Temporary files are never candidates. On startup, the lock holder MAY remove safe regular owned temporary files; unsafe entries cause a store diagnostic, never recursive deletion. A corrupt newest file MUST NOT silently cause restore of an older one: list marks it invalid; the caller must explicitly name a valid checkpoint. No parser repair, salvage or migration occurs during restore.

An interrupted write leaves the previous durable file intact. A file visible after rename but before successful directory fsync may or may not survive power loss. If present and valid it is inspectable, but a failed/lost reply MUST NOT be treated as proof of persistence. No claim is made beyond the filesystem’s fsync guarantees. Disk full, permission failures and size overflow leave the previous checkpoint untouched; a live mutation is not rolled back because its subsequent periodic checkpoint failed. An explicit checkpoint reports failure, while doctor records last attempt and last durable success.

Clean SIGINT/SIGTERM shutdown first follows RFC-0007: reject mutations, terminate supervised groups with the existing 2000 ms deadline, and reap. It then attempts a final checkpoint with shutdown="clean" only if cleanup succeeded, including observed final exits. Final persistence has a separate 2000 ms deadline, cannot delay process termination, and on failure shutdown reports operational failure. Hold the socket/store lock through this final phase and release it only after normal socket/owner-file cleanup. When recovery is enabled, this explicitly extends the total shutdown budget in RFC-0007 §2 and RFC-0009 §4 to at most 4000 ms: 2000 ms for existing process termination/drain, then at most 2000 ms for persistence. Disabled recovery retains the original bound. Uninterruptible kernel I/O is an operational failure, not proof that bounded shutdown succeeded. A pre-shutdown or periodic checkpoint has shutdown="open"; it MUST NOT claim clean exit. A crash uses the last published checkpoint and can lose every change since capture. Neither marker proves whether escaped descendants or external jobs still exist.

4. Durable formats 1, 2 and 3

The outer JSON envelope has exactly format (integer 1, 2 or 3, see below), payload (string) and sha256 (64 lowercase hex characters). Writers emit format 3 (section 4.2); readers accept 1, 2 and 3. SHA-256 covers the exact UTF-8 bytes of the decoded payload string, without a BOM or added newline. Payload is itself a JSON document with the shape below. The checksum detects damage; it is not authentication against a process that can rewrite the store.

This is a complete format-1 example payload, before wrapping/checksumming (sections 4.1 and 4.2 have format-2 and format-3 ones):

{"checkpoint":"c01234567-89ab-4cde-8fab-0123456789ab","generation":1,"socket":"/run/user/1000/venomx.sock","uid":1000,"captured_at":"2026-11-06T22:00:00.000Z","shutdown":"open","sessions":[{"session":"s0","name":"work"}],"windows":[{"window":"w0","session":"s0","name":"shell","revision":1,"nodes":[{"kind":"pane","pane":"p0"}]}],"panes":[{"pane":"p0","window":"w0","session":"s0","cols":80,"rows":24,"state":"running","text":{"availability":"omitted"}}]}

This script creates a valid envelope from a payload saved as payload.json (set FORMAT = 2 or FORMAT = 3 for a format-2 or format-3 payload):

import hashlib
import json
from pathlib import Path
FORMAT = 1
payload = Path("payload.json").read_text(encoding="utf-8").rstrip("\n")
envelope = {"format": FORMAT, "payload": payload,
            "sha256": hashlib.sha256(payload.encode("utf-8")).hexdigest()}
Path("checkpoint-example.json").write_text(
    json.dumps(envelope, ensure_ascii=False, separators=(",", ":")) + "\n",
    encoding="utf-8")

This is an offline fixture, not a supported import command. Production files are written only by the daemon’s secure writer.

Required payload fields and validation:

Field Type / rule
checkpoint c followed by lowercase UUID v4; matches filename
generation Integer 1–9007199254740991
socket, uid Absolute socket string and nonnegative UID; match active store
captured_at UTC RFC 3339 with milliseconds
shutdown open or clean
sessions Array of {session,name} with RFC-0007 IDs and names
windows Array of RFC-0007 window objects, including revision,nodes. Split nodes have no ratio in formats 1 and 2; format 3 allows it (section 4.2)
panes Array of pane records described below

Pane records require string pane,window,session, integers cols,rows in 1–65535, state (running or exited), and text. Only exited may include exit_code (0–255) or signal (a symbolic signal name supported by the writing platform), mutually exclusive. Readers accept a signal matching SIG[A-Z0-9]+ even when their platform cannot generate it; it is historical display data, never a signal target. Neither field means unknown historical status; never synthesize one from OSC. Clean checkpoints MUST contain no running pane. Each pane belongs to exactly one window and session; every tree leaf refers to exactly one pane in that window; every pane appears exactly once. Containers may be empty. Reject missing parents, duplicate IDs, invalid preorder indexes, cycles, invalid axes, extra/orphan nodes and revision/counter overflow. Arrays are sorted by numeric source ID. IDs are historical labels, not grants.

Text is one of these closed shapes:

  • {"availability":"omitted"}: no terminal bytes saved (default).
  • {"availability":"expired"}: retained text was discarded for age.
  • {"availability":"plain","saved_at":"2026-11-06T22:00:00.000Z","expires_at":"2026-11-07T22:00:00.000Z","screen":["ready"],"history":["previous output"]}.

For .plain, screen contains exactly rows physical rows (the short shape above only illustrates field types); history is oldest first. Both come from the primary screen, even while the alternate screen is active: the alternate screen is transient program UI, has no history and is never saved, and restore (section 5) hydrates the primary screen. Capture MUST NOT change the live terminal to read it. Each string is UTF-8 plain cell text, without C0/DEL/C1 control characters, terminal escape sequences, embedded CR/LF, styles, hyperlinks, clipboard or parser continuation. Tabs are expanded and width-zero continuation cells omitted using RFC-0007’s capture rules. History is capped by both configured rows and bytes, dropping oldest rows first. Screen rows are preserved together; if they alone exceed the text budget, omit the entire text object content with availability omitted. Do not drop topology to fit text: omit pane text in reverse numeric pane order until the whole checkpoint fits, or fail if topology alone is too large.

saved_at records the time the text was first captured; expires_at is no later than saved_at plus the then-configured text age. Recheckpointing archived text MUST preserve these original timestamps, not extend its lifetime. On restore, expire text at the earlier of stored expiry and saved_at plus the current configured age, or when current config forbids text. A backward clock (now < saved_at) MUST discard text rather than extend its lifetime. A future checkpoint capture time makes the entire checkpoint unavailable until explicitly forgotten or the clock catches up. Expired text never prevents topology restore; its runtime availability becomes expired and contents MUST be discarded.

Readers MUST reject duplicate JSON keys, invalid UTF-8, trailing data, checksum mismatch, unknown fields at any durable-format level, invalid enums and unsafe integers. Maximum file length is min(configured limit, 64 MiB); nesting depth is at most 32, and total sessions/windows/panes/nodes at most 100000. Bounds MUST be checked before unbounded allocations or traversals. The reader checks format before anything else in the envelope: a non-integer is corrupt, and an integer other than 1, 2 or 3 (0, negative, 4 or more) is incompatible (unsupported.checkpoint_format); no best-effort interpretation. Each file is read strictly under the rules of its own format: a format-1 file that contains a format-2 field, or a format-1 or format-2 file that contains a format-3 field, is corrupt, never upgraded. A future compatible wire member does not change this strict durable-file rule. Format evolution requires an explicit new reader and migration specification (section 4.1 is the one for 1 to 2, section 4.2 the one for 2 to 3), never execution of serialized code.

The writer uses an allowlist, not a dump of in-memory structs. It MUST exclude owner/bearer tokens and hashes, principal IDs/ancestry/bindings, grants and audit logs, PID/PGID/start times, environment, argv, cwd, shell history files, PTY raw rings, event cursors/logs, pane stream offsets/epochs, opaque emulator snapshots, OSC observations and pending input. Names and terminal text are user content; they are not promoted into trusted authority or executable instructions. It also excludes popups (section 4.1), float geometry as resolved against config or a client’s area, presentation clients, popup tickets and registered presets.

4.1 Format 2: floats and pane tags

Format 2 is format 1 plus three optional pane-record fields. Everything else in section 4 (envelope, checksum, payload fields, topology, text, bounds, strictness) is identical. Both are written only when they differ from the default, so a format-2 file of tiled, untagged panes has the same payload as format 1 with only the envelope’s format changed.

Pane field Rule
layer Only the string "float"; tiled panes omit it (writing "tile" is corrupt). Present exactly when float is
float Closed object: required z, integer 1–9007199254740991; optional preset, width, height, anchor, x, y, min_cols, min_rows, border, title
tags Array of 1–16 closed {key,value} objects, sorted strictly ascending by key bytes (so no duplicates); omitted when the pane has no tags (an empty array is corrupt)

float holds the pane’s RFC-0016 §8.2 spec: its preset name and the explicit geometry fields its creator passed, never the geometry resolved against config or a client’s window. A field is present only when it was given explicitly. preset is a namespaced id (<namespace>.<name>, like segment ids in RFC-0016 §5.1). width/height are strings, a cell count ("12", 1–65535) or a percent ("60%", 1–100), as in config. anchor and border are the RFC-0016 §8.2 enum names without the ZON dot ("top_left", "rounded"). x/y are integers in −4096–4096, min_cols/min_rows in 1–65535, and title is at most 128 bytes of UTF-8 without control characters. z is the saved z-order; only its relative order is used on restore (section 5).

A float pane is in its window’s float layer, never in the split tree: every tree leaf refers to a pane without layer, every such pane appears exactly once in its window’s tree, and a float pane appears in no tree. cols/rows are the float’s PTY size at capture.

tags is the pane’s RFC-0016 §4 map, the daemon-set owner included; keys and values follow RFC-0016 §4 syntax. More than 16 tags is a size failure (unsupported.checkpoint_size); any other bad tag is corrupt. Tags are labels for products and presentation, not permissions: restore never derives grants, ownership or principals from them (section 5).

Popups are never checkpointed. A popup belongs to one presentation client and closes when that client detaches (RFC-0016 §8.1); no checkpoint contains it, so layer is never "popup" (such a record is corrupt).

1 to 2 migration. There is no offline rewrite: the format-2 reader reads a format-1 file directly, as every pane tiled (no layer/float) with no tags, and rejects layer, float or tags in it as unknown fields. Restore then gives each such pane the restoring principal’s tag namespace as its owner (section 5). The next checkpoint after that restore is written in the current format (format 3, section 4.2). A daemon that only reads format 1 refuses a format-2 file as incompatible rather than misreading it.

Complete format-2 example payload (two tiled panes split by columns, two floats, tags), byte for byte what venomx 32d5d3d writes for this state:

{"checkpoint":"c01234567-89ab-4cde-8fab-0123456789ab","generation":2,"socket":"/run/user/1000/venomx.sock","uid":1000,"captured_at":"2026-11-07T21:00:00.000Z","shutdown":"open","sessions":[{"session":"s0","name":"work"}],"windows":[{"window":"w0","session":"s0","name":"agents","revision":2,"nodes":[{"kind":"split","axis":"cols","first":1,"second":2},{"kind":"pane","pane":"p0"},{"kind":"pane","pane":"p1"}]}],"panes":[{"pane":"p0","window":"w0","session":"s0","cols":40,"rows":24,"state":"running","tags":[{"key":"group","value":"thread-7"},{"key":"owner","value":"venom"},{"key":"role","value":"agent"}],"text":{"availability":"omitted"}},{"pane":"p1","window":"w0","session":"s0","cols":39,"rows":24,"state":"exited","exit_code":0,"tags":[{"key":"owner","value":"venomx"}],"text":{"availability":"omitted"}},{"pane":"p2","window":"w0","session":"s0","cols":64,"rows":19,"state":"running","layer":"float","float":{"preset":"venom.settings","z":2},"tags":[{"key":"owner","value":"venom"},{"key":"role","value":"control"}],"text":{"availability":"omitted"}},{"pane":"p3","window":"w0","session":"s0","cols":48,"rows":12,"state":"running","layer":"float","float":{"width":"60%","height":"12","anchor":"bottom_right","x":-1,"y":-1,"border":"single","title":"build log","z":5},"tags":[{"key":"owner","value":"venomx"}],"text":{"availability":"omitted"}}]}

p2 is a venom control view opened with --preset venom.settings and no explicit geometry; p3 is a float the human opened with explicit geometry and no preset. Wrapped with FORMAT = 2 by the script above, it decodes; wrapped with FORMAT = 1, it is corrupt.

4.2 Format 3: split ratios

Format 3 is format 2 plus one optional split-node field. Everything else in sections 4 and 4.1 (envelope, checksum, payload fields, pane records, floats, tags, topology, text, bounds, strictness) is identical.

Split node field Rule
ratio Integer 1–9999, RFC-0007 §4’s first child’s share in 1/10000; omitted when the split has none. A reader accepts every value in 1–9999, including one that draws an equal split (such as 5000)

A format-3 split node has exactly kind, axis, first, second and optionally ratio, the same closed shape as RFC-0007 §4’s wire node. The writer copies each split’s live ratio and omits it when the split has none, so a format-3 file of a layout nobody resized has the same payload as format 2 with only the envelope’s format changed. A ratio that is not an integer in 1–9999 (0, 10000, negative, fractional, a string) is corrupt; ratio on a pane node is an unknown field and corrupt. The reader does not check a saved ratio against pane minimums or any client’s area: like a live ratio, it is applied by RFC-0007 §4’s formula wherever the window is drawn.

2 to 3 migration. There is no offline rewrite: the format-3 reader reads a format-1 or format-2 file directly under that file’s own rules, and rejects ratio in it as an unknown field. Every split in such a file restores as an equal split (no ratio). The next checkpoint after that restore is written as format 3. A daemon that only reads formats 1 and 2 refuses a format-3 file as incompatible rather than misreading it.

Complete format-3 example payload (three tiled panes: p0 on the left at 62.5% of the columns, p1 over p2 on the right with an equal split, so only the root split has a ratio):

{"checkpoint":"c01234567-89ab-4cde-8fab-0123456789ab","generation":3,"socket":"/run/user/1000/venomx.sock","uid":1000,"captured_at":"2026-11-09T12:00:00.000Z","shutdown":"open","sessions":[{"session":"s0","name":"work"}],"windows":[{"window":"w0","session":"s0","name":"agents","revision":4,"nodes":[{"kind":"split","axis":"cols","first":1,"second":2,"ratio":6250},{"kind":"pane","pane":"p0"},{"kind":"split","axis":"rows","first":3,"second":4},{"kind":"pane","pane":"p1"},{"kind":"pane","pane":"p2"}]}],"panes":[{"pane":"p0","window":"w0","session":"s0","cols":50,"rows":24,"state":"running","tags":[{"key":"owner","value":"venomx"}],"text":{"availability":"omitted"}},{"pane":"p1","window":"w0","session":"s0","cols":29,"rows":12,"state":"running","tags":[{"key":"owner","value":"venomx"}],"text":{"availability":"omitted"}},{"pane":"p2","window":"w0","session":"s0","cols":29,"rows":11,"state":"running","tags":[{"key":"owner","value":"venomx"}],"text":{"availability":"omitted"}}]}

Wrapped with FORMAT = 3 by the script in section 4, it decodes; wrapped with FORMAT = 2 or FORMAT = 1, it is corrupt.

5. Explicit atomic restore

recovery.restore requires current admin and an empty daemon resource tree, including no retained panes or empty containers. It validates the named file completely, prepares all resources and events, then commits them atomically in one reactor turn. All sessions/windows/panes get fresh runtime IDs from the current monotonically increasing counters; window revisions restart at 1. Results give an old-to-new mapping. IDs consumed on failed preparation remain consumed. Snapshot consumers MUST never see a partial restored graph.

Every resource is owned by the current resolved restoring principal; no ownership string or self scope is imported. Archives have no process-backed principal. A relaunch issues a fresh RFC-0005 pane principal/token only when a new child is actually prepared. Owner and unknown remain the new daemon’s special identities; registry entries from the old daemon MUST NOT be recreated. The current unknown policy remains read-only at most, including its existing configured read scope. Reauthorization of agents requires explicit new grant.issue or new spawn grants; restore does not infer grants from names, labels, old resource IDs or archived content.

For each archive, recovery metadata is:

{"checkpoint":"c01234567-89ab-4cde-8fab-0123456789ab","previous_pane":"p0","process_state":"interrupted","text_state":"omitted","cut_rows":0}

cut_rows is a nonnegative safe integer counting saved screen and history rows that venom-term’s literal hydration kept cut at the last column because no single layout reproduced the saved row (for example, mode 2027 changed mid-row). It is zero for exact hydration or blank/omitted text. A positive count leaves text_state="plain" but MUST be shown with archive status outside terminal cells; doctor MUST report the sum across retained archives and mark the archive check as warning when nonzero. This is a report of this restore, not a durable pane field: checkpointing the resulting literal cells does not carry the count into another restore. The count remains available for the archive’s lifetime, including after its text expires.

process_state is exited when an exit was recorded, otherwise interrupted. It says nothing about whether an escaped process survives. The metadata is an optional closed recovery object on pane.list entries, pane.capture results and RFC-0003 pane snapshot content. Ordinary live panes omit it. Retained known exit code/signal is exposed by existing capture/EXIT fields. Interrupted archives have no exit code/signal. Recheckpointing an archive MUST preserve its historical process_state, not convert it into an observed clean exit. For this purpose durable pane records MAY additionally contain string previous_state (exited or interrupted) only with state="exited"; when present it overrides the archive’s process_state on subsequent restore.

Floats (format 2). A float record restores as an archive in its window’s float layer, never into the split tree. Its saved spec (preset name plus explicit fields) is resolved against the restoring daemon’s presets and config, exactly like a new pane.float: the saved geometry is never reused, and an unknown preset falls back to floats.default and is reported by doctor (RFC-0016 §8.2). Restored floats get fresh z-orders above the daemon’s current z counter, in their saved stacking order (saved z ascending, ties by numeric source pane ID): with counter n and k floats, they get n+1…n+k and the counter becomes n+k. An archive keeps its layer: pane.place on it fails conflict.layer until it is relaunched. pane.relaunch of a float archive puts the new pane in the same float layer with the same spec and z; the tree is unchanged, so there is no window.layout_changed and no revision increment.

Split ratios (format 3). Each restored split node keeps its saved ratio, so the restored window draws the same shares; the restored tree, ratio included, is what window.created and snapshots carry. Every split read from a format-1 or format-2 file restores as an equal split. Restore does not recompute or drop a saved ratio; RFC-0007 §4’s rules for removing a ratio apply only to later pane.split and pane.place.

Tags (format 2). Saved tags are restored as saved, owner included, so a product still finds its archives by owner/role/group. A record with no owner tag, which includes every format-1 record, gets owner = <restoring principal's namespace> (venomx for the human owner, RFC-0016 §5.1). Tags carry no authority: restore never derives grants, resource ownership, principals or a namespace from them, and a restored owner value does not let that namespace act on the pane. A relaunched pane does not inherit the archive’s tags; it gets a fresh owner from its creating principal like any new pane.

Restore initializes a fresh reset terminal at saved dimensions. When valid plain text exists, hydrate cells through venom-term’s literal-text interface, never feed checkpoint bytes to an escape-sequence parser. Use default styles, cursor at home and primary screen; no terminal queries, OSC or side effects. Otherwise the screen/history is blank. Display the archive status outside terminal cells. Plain text is deliberately lossy: alternate screen, palette, cursor, keyboard modes, partial UTF-8 and applications are not restored. pane.capture reads this screen under its normal read grant; no bulk durable-history verb is added. RFC-0006 emulating attaches can backfill the hydrated history as normal.

A daemon has a fresh venomx/<uuid> source before any connection is accepted, whether or not it later restores. Restore does not import or change source. Old tokens fail denied.token; invalid auth MUST NOT fall back to unknown. A valid new client using an old RFC-0003 cursor receives a fresh scoped snapshot with reason="source_changed". With an old RFC-0006 source/offset, ATTACH to a new mapped pane gets resumed=false, a newly encoded negotiated snapshot and READY, then EXIT (archives are exited). The fresh ring starts at offset 0, and row addresses/stream epochs are newly allocated. Requesting an unmapped old ID is an ordinary authorized not_found.pane; callers must rediscover, not assume numeric equality. No raw ring or serialized ghostty blob is loaded.

6. New verbs, schema and errors

Declare these in the same RFC-0004 format-1 product manifest as other verbs. All are nonstreaming, have closed typed results and standard exit policy. The closed archive recovery object on pane results and snapshots includes required integer cut_rows as specified in §5; it does not change the durable format or the mapping-only recovery.restore result. Dotted names map to spaced CLI paths; no ad hoc dispatch or new grant name.

Verb Parameters Result Dispatch grant / handler checks
recovery.checkpoint None {checkpoint,generation} admin; enabled
recovery.list None {checkpoints:[{checkpoint,status,generation?}]} admin
recovery.inspect Required string checkpoint {checkpoint,generation,shutdown,sessions,windows,panes,text_panes,expired_text_panes} (counts are integers) admin; no terminal content returned
recovery.restore Required string checkpoint {checkpoint,sessions,windows,panes} (mapping arrays) admin; enabled and empty tree
recovery.forget Required string checkpoint {} admin; explicit one-file deletion and directory sync
pane.relaunch Required pane, argv, boolean ack_orphans; optional grants {pane,window,session,principal,pid} spawn on pane plus write on pane; archive only; ack_orphans must be true

Mappings are arrays of closed {previous,current} string objects for each kind; counts and generations are nonnegative safe integers. List status is valid, corrupt, incompatible, expired, unsafe or future; generation is omitted when not safely decoded. Listing reveals no payload from invalid files and sorts decodable generations ascending, then unknown ones by ID. Inspect and restore accept only valid files, including valid topology whose text expired. A nonexistent or malformed checkpoint identifier never becomes a filesystem path. Authorization precedes file lookup, state inspection and existence disclosure. pane.relaunch’s dispatch grant is spawn; its additional scoped write check MUST occur before archive lookup or mutation.

Relaunch allocates a fresh pane ID from the current counter and atomically replaces the archive leaf with that pane, incrementing the window revision. It discards the old screen, history and recovery metadata and creates a reset live pane owned by the current relaunching principal. It uses RFC-0007’s PTY/principal/exec barrier and rollback contract, including explicit argv and RFC-0005 optional grants. Until successful commit the archive remains readable. On failure leave it unchanged, revoke staged principals and reap staged children. On success emit the normal principal issuance event, archive pane.removed, new pane.created, then window.layout_changed in the same atomic commit (a float archive has no leaf: section 5, no window.layout_changed). Existing streams of the removed archive close with CLOSE.reason="pane_removed"; clients rediscover the new leaf. No input is carried into the new process. The old pane ID is never reused within the source, so an archive’s old offset cannot resume a replacement process, even if both have offset 0. Concurrent relaunch requests serialize: at most one commits; the others get not_found.pane after the winning replacement removes the old ID. Retry after a lost success reply still names the removed archive ID and cannot launch again, including after the new process exits. An ordinary exited pane is not an archive and remains ineligible for this verb. The ack flag acknowledges that external jobs/escaped descendants may remain and that this new execution could duplicate external work; it does not certify cleanup. There is no automatic retry or launch during restore, startup or reconnect.

Example wire exchange after current authentication, on an empty fresh daemon:

{"id":2,"verb":"recovery.restore","params":{"checkpoint":"c01234567-89ab-4cde-8fab-0123456789ab"}}
{"id":2,"ok":{"checkpoint":"c01234567-89ab-4cde-8fab-0123456789ab","sessions":[{"previous":"s0","current":"s0"}],"windows":[{"previous":"w0","current":"w0"}],"panes":[{"previous":"p0","current":"p0"}]}}
{"id":3,"verb":"pane.relaunch","params":{"pane":"p0","argv":["/missing/program"],"ack_orphans":true}}
{"id":3,"error":{"code":"unavailable.spawn","message":"executable unavailable","data":{}}}

CLI --json unwraps ok; errors retain RFC-0001’s {"error":...} shape. Vanished executables affect only explicit relaunch, never topology restore. A second restore (including retry after a lost reply) fails on the nonempty tree, without duplicate resources. A successful restore is automatically eligible for the next periodic checkpoint but does not delete its input file. Cancellation before publication/restore commit discards staged work; after commit the result stands. Clients that lose a reply inspect current state.

Error Exit Condition / data
invalid_params.checkpoint 2 Invalid ID syntax; param="checkpoint"
invalid_params.ack_orphans 2 False acknowledgement; param="ack_orphans"
not_found.checkpoint 3 Authorized ID absent; empty data
denied.admin, denied.spawn, denied.write 4 RFC-0005 right/resource/principal data
conflict.recovery_busy 5 Explicit write already in flight; empty data
conflict.not_empty 5 Restore target not empty; empty data
conflict.not_archive 5 Relaunch target is not an archive; empty data
conflict.checkpoint_corrupt 5 Checksum/schema/topology/duplicate-generation failure; empty data
conflict.checkpoint_expired 5 Whole checkpoint expired or future-dated; empty data
unavailable.recovery_disabled 6 Checkpoint/restore disabled; empty data
unavailable.recovery_io 6 Store unsafe, I/O or durability failure; empty data
unavailable.spawn 6 New execution failed; empty data
unsupported.checkpoint_format 10 Unrecognized durable version; empty data
unsupported.checkpoint_size 10 Whole-file/count/depth bounds exceeded; empty data

Shared invalid argv, denied delegation, not-found pane, shutdown, cancellation, frame-size and allocation-exhaustion errors retain RFC-0007/0002 semantics. Do not include payload, paths, terminal content, raw argv or credentials in errors, events or doctor, except RFC-0007 §5’s derived argv0 in the pane entity and pane.created. Error messages are stable descriptions, not parser dumps. A failed restore emits no resource/recovery-success events.

7. Orphan processes and supervision

Clean restart terminates only the live process groups owned by that daemon, using its in-memory supervision handles and RFC-0007’s existing rules. It cannot preserve a PTY master across daemon death. Forced death may leave children, escaped descendants or remote work alive; PTY hangup is not proof that all work stopped. Subreaper ownership does not transfer to a new daemon.

A new daemon MUST NOT adopt, signal, wait on, attach credentials to or infer ownership of an old process from a saved PID, PGID, start time, executable name, terminal device or matching command line. Those fields are not stored at all. Orphan cleanup requiring verifiable OS containment (for example an external supervisor’s retained handle) is deferred; v1 reports cleanup_unverified after any restore, even for a clean checkpoint, and requires human inspection outside venomx. It MUST NOT claim a process sandbox or promise no external duplicate work after explicit relaunch. An unrelated process reusing a former PID MUST remain completely untouched. Old descendants connecting to the new daemon use fresh RFC-0005 peer resolution and at most unknown rights without new authorization; old tokens and process bindings confer no rights.

8. Events and conformance

Restore commits ordinary session.created, window.created and pane.created events for new resources in numeric session/window/pane order (containers before children), followed by pane.exited for each archive. A restored float’s pane.created carries layer = "float", its window and its resolved float geometry and new z, as RFC-0016 §8.3 specifies for pane.float. Known exit codes are included; interrupted archives omit them. No program.status_changed or command.started/finished is reconstructed from old terminal text. All events and restored topology become visible together at the commit boundary.

Extend the product event descriptor/catalog with these fields and entries; existing field types MUST NOT change:

Type Resource Data Snapshot entity
recovery.restored server:root checkpoint string, cleanup_unverified=true boolean None (historical completion)

recovery.restored requires read on server:root for event delivery; the mutation still requires admin. Per-pane snapshot recovery metadata is available only to readers of that pane. Relaunch uses the existing pane/window catalog and snapshot mappings. The shared descriptor’s recovery is the optional closed object of section 5. Success/failure of disk writes is exposed through doctor, not new event spam. Events are not replayed from checkpoint files.

COM-244 MUST reuse the shared modules. COM-255 (venom-term) supplies bounded literal text export/hydration; COM-256 (venom-std) supplies fallible config pre-activation effects for retention. Both explicitly block COM-244 and are governed by this accepted specification; product-local substitutes are not allowed. Neither depends on resolving COM-230 or COM-235.

COM-244 MUST run the shared agent-contract suite and real process/filesystem fault tests. The acceptance matrix is normative; “fresh” means different from the previous daemon’s authority/source, not merely an empty in-memory cache.

Case Required observation
Defaults / disabled No checkpoint writes or restore, explicit persistent zero-session behavior unchanged
On-demand interaction Restore never launches daemon; nonempty restore wins active transition only before grace/stopping commit; disk checkpoints do not count as sessions; shutdown timing follows enabled/disabled budgets
Clean restart Children terminated/reaped within foundation deadline; final durable clean marker only after success; explicit restore reproduces topology and known exits, no processes
Forced SIGKILL Last durable generation restores; later mutations may be lost; running records become interrupted archives, cleanup unverified
Interrupted writes Inject before write, short write, fsync, rename and directory fsync; prior generation survives; no temporary file selected; no false durable acknowledgement
Corrupt/partial state Truncation, bad checksum, duplicate keys/IDs, invalid tree, duplicate generation fail atomically; explicit selection required for older valid file
Version/limits Unknown version (0, 4+), oversized/count/depth-bound input and future timestamp produce documented errors without allocations beyond caps
Format 2 Format-2 files read and restore; format-1 files still restore (all tiled, owner = restoring namespace); format-2 fields in a format-1 file, a float in a tree, layer other than "float", z < 1, invalid spec fields, unsorted/empty/duplicate tags fail; tiled + floats + popup checkpoint restores the floats in the float layer with fresh z in saved order and no popup
Format 3 Writer emits format 3 and each split’s ratio, omitting it for equal splits; a checkpointed resized split restores with the same ratio; format-1 and format-2 files still restore as equal splits; ratio in a format-1 or format-2 file, on a pane node, or outside 1–9999 fails; a format-2-only reader refuses format 3 as incompatible
Topology Nested rows/cols, empty containers, exited panes and removed leaves retain membership invariants; fresh IDs/mapping and revision 1; allocation failure publishes nothing
Text / expiry Off stores no terminal bytes; plain preserves literal Unicode rows and clipped history; expiry/backward clock/current off drops text but restores topology; rechkpt never renews expiry
Secret exclusion Seed tokens, token hashes, env, argv, cwd, OSC/credentials in runtime structs; none appear in checkpoint allowlist, errors, events or doctor; opted-in terminal text remains explicitly sensitive user content. The one exception is RFC-0007 §5’s derived argv0 (basename of argv[0], at most 255 bytes), allowed only in pane.list results, pane.created data and the pane entity; a canary in the directory part of argv[0] (anything up to its last /), in the bytes past 255 or in another argv element still appears nowhere, and checkpoints and restored archives have no argv0
Credentials New owner token, empty old registry, new child principal/token on relaunch; old owner/bearer tokens denied; no old resource ownership or self-grants restored
Resume New authenticated old event cursor gets source_changed snapshot; mapped-pane old stream resume gets resumed=false and fresh snapshot/READY/EXIT; unmapped IDs fail normally
Grants Unknown/scoped non-admin cannot list/inspect/checkpoint/restore/forget; relaunch checks spawn AND write; excessive child grants denied; reauthorization explicit
Vanished executable Restore still succeeds; explicit relaunch returns unavailable.spawn, archive/tree intact, no child/principal leak
Duplicate launch Concurrent/retried restore rejected once tree nonempty; concurrent/retried relaunch launches at most once, including rapid exit and lost response
Orphans / PID reuse Crash with surviving descendants and a reused-PID unrelated process; no startup/restore signal, adoption or implicit launch; diagnostics never assert cleanup
Filesystem security Other UID, symlink, hard link, permissive store, replaced file, disk full and prune failure tested; no traversal, unsafe reads or silent success
Retention/config Count/age pruning synced; valid last generation survives failed write; off transition removes text checkpoints; failed purge/reload reported and active policy unchanged
Events/clients Restore snapshot race exposes all or none; no OSC replay; relaunch closes old streams, old pane ID cannot resume new process; per-resource grants filter snapshots/events

Agent-contract impact

  • Verbs/schema: section 6 adds generated CLI/MCP/docs/completions from one manifest; recovery handlers require current authority, not serialized intent.
  • JSON/exits: standard results/errors/exits remain; new errors are section 6. No startup banner or persistence diagnostic may pollute JSON stdout.
  • Events: section 8; sources/cursors remain RFC-0003, binary resumes RFC-0006.
  • Skill: describe archives, explicit fresh argv/grants, lost-reply inspection, no automatic restore/relaunch, opt-in text privacy and process limitations; continue recommending verbs/events for agents rather than attaching.
  • Doctor: add recovery.store safety/writability, enabled/effective bounds, last attempt and durable-success time, last error code, retained/invalid/ expired counts and bytes, archive count and cleanup_unverified status. Unsafe/unwritable enabled stores or failed writes are errors; disabled stores are informational; restored orphan uncertainty is a warning. Follow the existing doctor result-status contract and redact all user content.
  • Config: shared get/set/check/reload/schema exposes section 2 with defaults, enums/ranges and sensitive-text explanation. Config never initiates recovery.
  • Grants: no new rights; section 5 requires fresh identities/reauthorization. Default unknown remains read-only; opaque data never grants authority.
  • Headless/OSC: all operations run without a TTY; restored text cannot execute escapes or revive observations. Only new live authoritative PTYs parse OSC.

Alternatives

  • Do nothing: loses topology and historical evidence on restart, leaving COM-244’s behavior unspecified.
  • Implicit command replay: may duplicate external side effects and cannot restore program memory or authority. Explicit relaunch requires new intent.
  • Adopt a surviving PID/PGID: unsafe under PID reuse and missing ownership; retained OS supervision is a separate future design.
  • Persist raw PTY or ghostty snapshots: risks terminal side effects, secrets, parser compatibility and stale offsets. Plain text is lossy but format-independent.
  • Persist principals/tokens: contradicts RFC-0005’s v1 restart boundary.
  • Restore into nonempty sessions: requires merge and conflict policy; an empty target makes atomicity and ambiguous retries concrete without merging owners.
  • Always retain terminal text: unnecessarily saves sensitive output. Off is the default; plain content requires explicit config and bounded retention.

Open questions

  1. Terminal content policy: resolved by the maintainer: optional plain text with private permissions and retention limits. It does not guarantee removal of application secrets printed as terminal text.
  2. Binary terminal fidelity and stable snapshots: deferred to COM-230. Formats 1, 2 and 3 neither read nor require ghostty disk snapshots; any durable binary extension needs its own accepted format/migration/security contract.
  3. Persistent principals or changing unknown policy: deferred to COM-235. Fresh issuance and existing read-only defaults apply independently here.
  4. OS-backed adoption/orphan cleanup and merging into a live daemon: deferred beyond COM-244; no such behavior ships under this RFC. COM-244 must open a separately scoped spec follow-up before implementing either feature.
  5. On-demand daemon lifetime: coordinated with COM-241’s now-accepted RFC-0009. Recovery commands are never auto-start triggers. Prefer explicit persistent serve for inspection/restore. In an already running on-demand daemon, nonempty restore transitions awaiting_first_session to active at its atomic commit; empty restore does not cancel or extend the 5000 ms grace. Expiry/stopping and restore serialize exactly like session creation: once stopping wins, authorized restore fails unavailable.shutdown without side effects. Existing sessions/archives count normally; disk checkpoints alone do not keep the daemon alive. Last-session and grace-expiry shutdown use section 3’s final checkpoint rule, including the untouched-empty-start safeguard and opt-in durability deadline extension.

The maintainer accepted this RFC and its resolved/deferred questions on 2026-11-06, including opt-in terminal text and the final persistence deadline extension.