Venom
RFC-0007venom-spec

venomx daemon/session foundation

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

History

  • 2026-11-11 (venom-org/venom-spec#151, venom-org/venomx#178; compatible addition under RFC-0003 §6; explicitly accepted by the maintainer commixon on 2026-11-11): pane.list entries (§5) and the pane snapshot entity (§7) gain an optional process: the name of the PTY’s foreground process group leader, sampled by the daemon (§5 says how and how often). They also gain an optional boolean title_current, which says whether the OSC 2 title was set since the foreground last changed. §7 adds the pane.process_changed event. Clients that name a pane prefer a current title, then RFC-0003 §8’s cmdline, then process, then argv0. §9 adds verification cases. Requests and older readers are unchanged.
  • 2026-11-10 (venom-org/venom-spec#144, venom-org/venomx#147; compatible addition under RFC-0003 §6; accepted by venom-run (unattended, delegated by the maintainer) on 2026-11-10): pane.list entries (§5), pane.created data and the pane snapshot entity (§7) gain an optional argv0: the basename of the pane’s spawn argv[0], at most 255 bytes (§5 says how it is derived). RFC-0022 §3.2’s title line falls back to it when a pane has no OSC 2 title. It is never the directory part of argv[0] (anything up to its last /), the bytes past 255, or another argv element. It is also never RFC-0013 program, RFC-0015 shell text or the cwd, which all stay redacted. RFC-0010 §6 and §8, RFC-0012 §7, RFC-0013 §7 and RFC-0015 §9 each gain a narrow exemption for it. A shell run’s argv0 is sh. Checkpoints don’t keep it, so a restored archive pane omits it, as it omits pid and start. §9 adds its verification cases. Requests and older readers are unchanged.
  • 2026-11-09 (venom-org/venom-spec#131, venom-org/venomx#67; normative; accepted by venom-run (unattended, delegated by the maintainer) on 2026-11-09): §4 pane.split and tiling pane.place clamp an ancestor split’s ratio that would leave the child holding the new pane below its raised minimum, instead of removing it. Cells come only from that ancestor’s other child, which keeps its own minimum; the ratio is removed only when the cells can’t hold both minimums. Ancestors whose minimums the split did not raise keep their ratio. Removing it could squeeze the other side below its minimum and clip panes to zero while the viewport had room.
  • 2026-11-08 (venomx#44, normative; explicitly accepted by the maintainer on 2026-11-09): §4 split nodes gain optional ratio, the “arbitrary ratios” this section deferred, for RFC-0016 §7.7’s pane.resize. Compatible: a layout nobody resized is unchanged on the wire, and a client that ignores ratio draws equal shares. Such a client, while active on a pane, also pushes its equal-share PTY size on that pane, until venom-desk#15. A split that would leave a child below its minimum removes the ancestor’s ratio.
  • 2026-11-06: The maintainer accepted RFC-0009, adding opt-in on-demand startup and session-count lifetime to §2. The explicit persistent default and COM-238 foundation scope remain unchanged. RFC-0009 governs the additional mode when implemented by COM-242.
  • 2026-11-07: The maintainer accepted RFC-0012, which adds pane.kill and opt-in recursive window.remove / session.remove deferred by §5. Calls that do not opt in keep §5’s exited-only and empty-only rules. COM-246 implements it.
  • 2026-11-07: The maintainer accepted RFC-0013, which adds optional cwd, env and env_unset to window.create and pane.split, deferred by §5. It also makes every spawn set VENOMX_SOCKET, strip more inherited identity variables and follow stricter PATH search rules (RFC-0013 §9). COM-248 implements it.
  • 2026-11-07: The maintainer accepted RFC-0014, which adds pane.capture_rows, the bounded, paginated history-range capture deferred by §5. pane.capture and its unsupported.result_size rule are unchanged. COM-250 implements it.
  • 2026-11-07: The maintainer accepted RFC-0015, which adds run, run.wait, run.get, run.list and run.cancel, the trusted run/wait execution deferred in Alternatives. A run is a new pane whose result comes only from the daemon’s wait status; existing verbs are unchanged. COM-252 implements it.
  • 2026-11-08 (COM-305): RFC-0013 adds an optional program to window.create and pane.split, the file to execute when it differs from argv[0]. Requests without it behave as before.
  • 2026-11-08 (venom-org/venomx#47, compatible addition): pane.list entries of a live pane gain pid and start, the (pid, starttime) the pane’s principal is bound to. Another daemon needs them to tell a pane’s process from a reused PID (RFC-0019 section 7.2). Both are absent once the process has exited and for a pane restored without one.

Summary

Define the smallest runnable venomx product around the RFC-0006 pane-stream server: one daemon/CLI executable, explicit foreground startup, in-memory sessions and windows, authoritative split topology, scoped discovery and schema-defined mutations. COM-238 implements this foundation; COM-223 adds the emulating presentation client. This RFC adds product semantics to RFC-0001–0006 without replacing their transport, grants or event contracts.

The governing shared contracts are agent behavior, transport, events, verb schemas, identity/grants and pane streams. Review and acceptance follow PROCESS.md.

Motivation

COM-222 supplies a server component with PTYs, terminal emulation, snapshots and raw-byte streams. It does not install an executable or define sessions, windows, product verbs or daemon startup. COM-223’s acceptance test needs two real clients to find the same session and keep its processes alive after both clients detach. A pane-only demonstration can exercise the protocol but cannot establish that product behavior.

The foundation belongs in a separate issue and PR. Implementing sessions, config, process ownership and discovery inside the renderer would obscure the daemon/client boundary and duplicate work needed by venom and venom-desk. The product MUST reuse venom-std’s shared facilities and conformance suite.

Design

1. A runnable headless path

Run the daemon in one terminal:

venomx serve

From an outside shell, the human explicitly opts into owner authority for mutations, as specified by RFC-0005. IDs in these examples assume a fresh daemon and successful preceding commands:

VENOMX_TOKEN=@owner venomx session create --name work --json
VENOMX_TOKEN=@owner venomx window create --session s0 --argv '["/bin/sh"]' --json
VENOMX_TOKEN=@owner venomx pane split --pane p0 --axis cols --argv '["/bin/sh"]' --json
venomx session get --session s0 --json
venomx window get --window w0 --json

After COM-223 lands, each of two host terminals can run:

venomx attach --session s0

attach --pane p0 remains the direct single-pane presentation entry point. attach is not required to ship in COM-238. Every foundation operation MUST be testable without a TTY, including with stdin closed. Commands MUST NOT implicitly create a daemon, a session, a window or a shell.

2. Daemon startup and lifetime

serve MUST run in the foreground and use RFC-0002 socket selection, locking, permissions, peer identity and negotiation. Concurrent starts for one socket MUST yield one daemon and the standard lock-conflict failure. The same executable MUST implement serve, generated ordinary CLI verbs, mcp and, when implemented, presentation clients.

The initial mode is explicit persistent operation. A daemon MUST remain alive with zero sessions, zero running children or zero clients. Detaching, client crashes, socket disconnects and terminal closure MUST NOT terminate panes. A CLI or presentation client that cannot reach a daemon MUST return unavailable (exit 6), without auto-start or silently connecting elsewhere.

SIGINT or SIGTERM to serve initiates shutdown. The daemon MUST stop accepting mutations, supervise termination of its pane process groups, reap children and release transport, owner file and lock resources. It MUST send SIGHUP to each live pane group, wait at most 2000 ms using a monotonic deadline, then send SIGKILL to surviving groups and reap its children. Socket draining MUST be bounded by that same grace deadline; slow clients MUST NOT delay child termination. Already-exited panes need no signal. Successful clean shutdown follows RFC-0001’s serve --json behavior. An unkillable OS process is an operational shutdown failure, not evidence of successful cleanup.

The daemon MUST act as a Linux child subreaper and bind process principals as RFC-0005 requires. A pane group is established by PTY creation; the daemon MUST NOT accidentally signal its own process group. Escaped descendants and platform limitations follow RFC-0005 and MUST be reported by diagnostics rather than claimed to be a sandbox.

State and PTYs survive client loss, not daemon restart. Each start MUST generate a new venomx/<uuid> source shared by events and pane streams. Sessions, windows, principals and panes are memory-only in this version. On-demand/tmux-style startup, durable restore and crash recovery require later work; this RFC does not define flags or config values for those modes.

3. Ownership and containment

The daemon owns this resource tree:

server:root
  session:s0
    window:w0
      pane:p0
      pane:p1

A session is a named container of windows. A window owns one split tree. A pane is a leaf referring to one COM-222 pane, PTY and authoritative Vt. A pane MUST belong to exactly one window and session; linking, moving and sharing a pane between windows are outside this version. An empty session or window is valid. Creating a session creates no processes; creating a window creates its initial pane and process atomically.

Session and window IDs MUST be allocated from separate increasing counters as s0, s1, … and w0, w1, …. Pane IDs MUST continue using COM-222’s increasing p0, p1, … counter. IDs MUST NOT be reused within a source, including after deletion or failed creation. Counter exhaustion MUST fail before allocation. IDs, rather than names or array positions, select targets. Names are display strings, need not be unique, and MUST contain at most 128 UTF-8 bytes with no C0 or DEL controls. The default name is empty.

Each resource MUST record the resolved creating principal as owner. parentOf MUST implement the tree above for RFC-0005 containment checks. PTY processes get distinct pane principals with issuance, ancestry binding, attenuation and environment sanitization from RFC-0005. In particular, unknown clients remain read-only and a human’s owner token MUST NOT leak into children. No TUI-provided principal, OSC field or JSON member can replace the connection’s resolved identity.

Process exit MUST retain the pane, Vt, history and layout leaf. Exit status comes from wait status; signals MUST be represented by their symbolic name, with no invented successful exit code. Removing the final pane makes its window empty; neither the window nor session is removed automatically.

4. Shared topology, local presentation

window.get returns a flat tree so the closed RFC-0004 format-1 descriptor can represent it without recursive types:

{"window":"w0","session":"s0","name":"","revision":2,"nodes":[{"kind":"split","axis":"cols","first":1,"second":2},{"kind":"pane","pane":"p0"},{"kind":"pane","pane":"p1"}]}

The root is array index 0; an empty window has nodes=[]. Nodes are in preorder. A pane node has exactly kind and pane. A split node has exactly kind, axis, first, second, and optionally ratio. Child indexes MUST be valid, forward indexes; every non-root node has exactly one parent. Every pane in the window occurs once. axis=cols divides left/right; axis=rows divides top/bottom.

Of the cells a split’s one-cell separator leaves, avail, the first child gets ceil(avail * ratio / 10000) and the second the rest. ratio is an integer from 1 to 9999. Each child keeps at least one cell while avail is 2 or more. With no ratio the split is equal, with the first child getting the extra cell on odd dimensions. pane.resize (RFC-0016 section 7.7) sets it, and omits it when the result is the equal split, so a layout nobody resized has no ratio. pane.split and tiling with pane.place add a pane in the space the tree already has. If that raises the minimum (RFC-0016 section 7.7) of the child of an ancestor split that holds the new pane, and leaves that child below it, the ancestor’s ratio is clamped: its divider moves just far enough to give that child its new minimum, taking cells only from the other child, which keeps at least its own minimum. Only when the cells the ancestor divides can’t hold both children’s minimums is its ratio removed. An ancestor whose children’s minimums the split did not raise keeps its ratio, even when one of its children is already below its minimum, and so does an ancestor whose child that holds the new pane still has room. Ancestors are worked out from the root down, on the panes’ current PTY sizes as pane.resize measures them, so a clamp higher up gives the splits below it their new cells. A clamp that lands on the equal split omits ratio. Clipping otherwise stays as it was: a split without a ratio is left as it is, and a viewport too small for an equal split still clips panes to zero.

For example, p0 | (pa | (pb|pc)) 161 columns wide, with root ratio 62 and inner ratio 9807, draws p0=1 pa=155 pb=1 pc=1. Splitting p0 across needs three columns on the root’s first side, so the root’s ratio is clamped to give it three, and the other side keeps 157: p0=1 new=1 pa=153 pb=1 pc=1. Removing the ratio instead would draw pa=78 pb=0 pc=0.

{"kind":"split","axis":"cols","first":1,"second":2,"ratio":6250}

pane.split replaces the source leaf with a split: existing pane first, new pane second. pane.remove collapses its parent to the surviving sibling, or leaves an empty tree if it removes the sole pane. The window revision starts at 1 and increases on every topology change, without exceeding RFC-0002’s safe JSON integer range. The daemon MUST validate all invariants before publishing a topology.

The shared tree determines membership and arrangement. Active window, focused pane, scroll position, selection and physical rectangles belong to each presentation client. A small host terminal MUST clip or expose a local overflow indicator; it MUST NOT delete shared leaves or send zero dimensions. Positive viewport sizes and authoritative PTY sizing follow RFC-0006’s latest, smallest and fixed policies unchanged.

5. Minimal product verbs

All entries below MUST be declared in one RFC-0004 manifest, with typed parameters, results, errors and standard exit policy. Dotted names map to spaced CLI paths. Requests and responses use RFC-0002; CLI --json emits the unwrapped result. Every mutation MUST authorize before revealing target existence or producing side effects.

Verb Parameters Result Required authorization
session.create Optional name string, default empty {session,name} write on server:root
session.list None {sessions:[{session,name}]} read; return only directly readable sessions
session.get Required session string {session,name,windows:[{window,name}]} read on session
session.remove Required session {} write on session; must be empty
window.create Required session, argv; optional name, cols, rows, grants {window,session,pane,principal,pid} spawn on session
window.get Required window Section 4 object read on window
window.remove Required window {} write on window; must be empty
pane.list Optional session string {panes:[{pane,window,session,title,exited}]}; a live pane also has pid and start, its process as RFC-0005 section 3.3 binds it; a pane may have argv0, process and title_current (below) read; filter each pane
pane.split Required pane, axis, argv; optional cols, rows, grants {pane,window,session,principal,pid} spawn on source pane
pane.send Required pane, text string {bytes} write on pane
pane.capture Required pane {text,cols,rows,exited}, optional exit_code or signal read on pane
pane.remove Required pane {} write on pane; must have exited

IDs and text above are strings; dimensions and PIDs are integers, exited is boolean, and bytes is a nonnegative byte count. Lists MUST be ordered by their numeric ID counters, not lexical string order. The manifest MUST include complete object descriptors; braces in this table are descriptive, not a separate manifest language.

argv0. A pane that a spawn created (window.create, pane.split, and the spawning verbs of later RFCs such as RFC-0015 run, RFC-0016 pane.float and pane.popup, and RFC-0010 pane.relaunch) carries an optional string argv0 in pane.list, in pane.created data and in its pane snapshot entity (§7). The daemon derives it once, when the spawn succeeds, from the argv[0] it executes. It never uses RFC-0013 program or the resolved executable path:

  1. Take the bytes after the last / in argv[0], or all of argv[0] when it has no /. /usr/bin/nvim gives nvim, ./run.sh gives run.sh, and -sh (RFC-0017 §3’s login shell) gives -sh.
  2. If that is empty (argv[0] is / or ends in /), the pane has no argv0.
  3. If it is longer than 255 bytes, cut it to the longest prefix of at most 255 bytes that ends on a UTF-8 code-point boundary. Request strings are valid UTF-8 (RFC-0002), so the result is valid UTF-8 and not empty.

argv[0] here is the one the daemon actually executes. For an RFC-0015 shell run that is /bin/sh (the daemon runs ["/bin/sh","-c",<shell>]), so its argv0 is sh. It is never derived from the shell text.

The daemon keeps only this derived string. argv0 is never the directory part of argv[0] (anything up to its last /), the bytes past 255, or another argv element. It doesn’t change for the life of the pane: it stays after the pane exits, and an exec inside the child doesn’t change it. It is absent when unknown, in particular on a restored archive pane: RFC-0010 §4 checkpoints exclude argv, so restore has nothing to derive it from. A relaunch creates a pane with its own argv0. Readers MUST treat absence as unknown and MUST NOT fill it in from title.

argv0 is a display label, not an identity: any spawn holder chooses argv[0] freely (RFC-0013 §6), so clients MUST NOT use it to decide what program runs or what it may do, and render it as untrusted text, like title.

process. A live pane MAY carry an optional string process in pane.list and in its pane snapshot entity (§7): the name of its PTY’s foreground process group leader. The daemon reads the group with tcgetpgrp on the PTY master. It reads the name again only when the group ID changes: /proc/<pgid>/comm on Linux, proc_name on macOS, and on other platforms the field is absent. The name is per job, not per process: make stays make while it runs compilers, and a pipeline shows the leader of its group. The daemon cuts the name like argv0 (rule 3) and drops it when it is empty or not valid UTF-8. process is absent when unknown, in particular when the pane has exited, on a restored archive pane, or when the group can’t be read. Like argv0 it is a display label, not an identity: a program sets its own comm (Linux cuts it to 15 bytes), so clients MUST NOT use it to decide what program runs or what it may do, and render it as untrusted text.

Sampling. The daemon samples a live pane’s foreground group after it reads PTY output, when it parses an OSC 133 marker or an OSC 2 title, and on a slow timer, so that a silent command (sleep 100) is seen. It MUST bound the rate to at most 4 samples per second per pane, and the timer to at most 1 per second. It MAY run the timer only for panes that an attached client focuses. Sampling MUST NOT delay PTY bytes to clients or the parsers. When the daemon parses an OSC 2 title, it samples first, so a pane.process_changed that the title’s program caused comes before that pane.title_changed.

title_current. A live pane carries the boolean title_current in pane.list and its pane entity. It is true when the pane’s title was set by OSC 2 after its process and its RFC-0003 §8 active command last changed, and false otherwise, including when the pane has no title. A pane.title_changed sets it to true. A pane.process_changed, a command.started or a command.finished sets it to false. A client that follows events can therefore track it without a pane.list round trip.

Naming a pane. A client that shows what runs in a pane (RFC-0022 §2.8 and §3.2) SHOULD prefer, in order:

  1. title, when title_current is true;
  2. the cmdline of the pane’s active RFC-0003 §8 command;
  3. process;
  4. argv0;
  5. title, when nothing else is known.

A title belongs to whoever set it, so a shell’s title doesn’t hide the program that runs after it, and a program’s own title (an agent’s task line) still wins over its process name.

argv is a nonempty array of strings, with a nonempty first entry and no NUL bytes. It is executed directly, never implicitly through a shell. cols and rows default to 80 and 24 and are in 1–65535. The daemon MUST resolve the executable using its environment and inherit its working directory. Per-request cwd and arbitrary environment overrides are deferred; callers can explicitly execute a shell if shell behavior is intended. grants uses the optional typed array from RFC-0005 section 6.2, including its default interactive profile for owner-created panes. Spawn authorization is sufficient for the topology insertion inherent in creation/splitting; it MUST NOT be replaced by a weaker write-only check.

pane.send sends the exact UTF-8 bytes in text, with no implicit newline or key notation. Its bounded queue and ordering MUST be the same ones used by RFC-0006 INPUT. Successful {bytes} means the complete input was accepted into that queue, not executed by the program. An exhausted queue MUST reject the entire request before inserting any bytes. Binary and keyboard-protocol input from presentation clients remains RFC-0006 INPUT.

pane.capture reads only the active visible screen from the authoritative Vt. It MUST export text without terminal control sequences or styles, separate physical rows with LF and omit width-zero continuation cells. It MUST preserve cell text and spaces, with no final LF. It MUST NOT move any client’s viewport. Large results that cannot fit the negotiated frame MUST fail unsupported.result_size; history-range capture is deferred.

An explicit pane.list.session is authorized on that session before lookup. With it absent, list every authorized pane, including when the caller cannot read its parent session; returned ancestor IDs identify containment, not permission to inspect sibling panes. session.list does not expose names of unreadable sessions. window.get and session.get require their container’s read right. These container results expose membership IDs and display names as container metadata; they MUST NOT expose descendant terminal contents or credentials. Concrete container scopes include descendants under RFC-0005; a kind wildcard such as window:* does not grant read on panes. Each pane attach/capture MUST check its own right even after container discovery. A presentation client MUST show an unavailable placeholder for an unreadable leaf, without attempting owner elevation beyond RFC-0005. Direct attach --pane serves pane-scoped callers.

Creating a window or splitting MUST prepare its child principal, PTY, process, layout allocation and events before commit. Before exec, the parent MUST establish the ancestry binding and release a child-start barrier. An exec-status pipe MUST distinguish exec failure from a program that starts successfully and later exits 127. On preparation/exec failure, the daemon MUST terminate/reap any child, close its PTY, revoke the prepared principal and discard staged resources/events. IDs may be consumed; ghost panes and partial trees MUST NOT be visible. Fast successful process exit still creates an exited pane and ordered create/exit events.

This version removes only exited panes. Removing a running pane MUST fail conflict.pane_running; removing a nonempty container MUST fail conflict.not_empty, without terminating or deleting anything. Explicit live-pane kill and recursive removal are deferred. This keeps removal atomic without an unbounded synchronous wait or an implicit destructive cascade. Shutdown remains the separately defined process-group operation.

6. Errors

Shared transport, validation, authorization and cancellation errors retain their meanings and exits. Product declarations additionally include:

Code Exit Meaning
invalid_params.name 2 Invalid display name
invalid_params.argv 2 Invalid argument vector
invalid_params.size 2 Invalid dimensions
not_found.session, .window, .pane 3 Authorized target absent
denied.read, .write, .spawn 4 Required scoped right absent
denied.delegation 4 Requested child grants exceed creator’s authority
conflict.not_empty 5 Container still has children
conflict.pane_running 5 Removal requires an exited pane
conflict.pane_exited 5 Input or split requested on an exited pane
unavailable.spawn 6 PTY/process preparation or exec failed
unavailable.input 6 Entire input cannot be queued
unavailable.shutdown 6 Mutation attempted during shutdown
unsupported.result_size 10 Capture/list/layout result cannot fit the negotiated frame

Name/argv/size errors use data.param naming the failing parameter. Not-found, conflict, unavailable and result-size errors have empty data; grant error data follows RFC-0005. Diagnostics MUST NOT include tokens or raw environment contents. Allocation/ID/revision exhaustion MUST fail without committing partial state, using standard internal (exit 11). Protocol and hello errors remain RFC-0002. Stream errors remain RFC-0006.

7. Discovery and topology events

The product MUST extend its RFC-0003 event descriptor and catalog with:

Type Resource Data Snapshot entity
session.created session:<session> session, name session: {session,name}
session.removed session:<session> session Session disappears
window.created window:<window> Complete section 4 object window: complete section 4 object
window.layout_changed window:<window> window, session, revision, nodes window: complete section 4 object
window.removed window:<window> window, session Window disappears

The product MUST preserve the fixed types of existing descriptor fields and add these names with the same types in every event. nodes is an array of the closed node descriptor from section 4, with per-kind field rules validated by the publisher. Window/session events are authorized against their own resource. Their snapshots include current state even when no retained event exists. Empty containers remain snapshot entities. A session snapshot contains only its own ID and name; window membership is represented by window entities and queried by session.get. Event type selection MUST select these current entities according to RFC-0003’s catalog mapping, not reconstruct them from historical events.

Creation MUST commit state and append events in one reactor turn: session creation emits session.created; window creation emits window.created, then existing pane.created; splitting emits pane.created, then window.layout_changed. window.created carries the initial tree in its data, so no redundant initial layout event is required. Pane removal emits RFC-0003’s observation cleanup and pane.removed, then window.layout_changed. Container removal emits its removal event. The relevant grant containment/ownership data MUST remain available until removal events have been authorized for existing subscribers. Event snapshot capture cannot observe half of a mutation. Root-process exit emits the existing observation/pane.exited sequence; layout is unchanged.

venomx extends RFC-0003’s pane.created data and pane snapshot entity with the optional string argv0 of §5. It is in pane.created data and in the entity exactly when the pane has one, so a client that renders titles from events needs no pane.list round trip. pane.title_changed and pane.exited don’t carry it, and it never changes, so no event updates it. The name is new in the shared descriptor, with the single type string.

venomx adds this event to its catalog, with the process and title_current fields of §5 on the pane entity:

Type Resource Data Snapshot entity
pane.process_changed pane:<pane> pane string, optional process string pane

The daemon emits it when a sample (§5) gives a different process from the last one, including a change to or from absent. A sample that gives the same name, or a group change whose leader has the same name, emits nothing. pane.exited implies that process is absent, so no pane.process_changed follows it. process is new in the shared descriptor with the type string, and title_current with the type boolean.

For pane.exited, normal exit uses exit_code; signal death omits it. The optional signal in product capture results and RFC-0006 EXIT does not invent an exit code in RFC-0003’s existing pane event/entity.

A session presentation client SHOULD subscribe to window.* with empty resources (the authorized projection), stage the initial RFC-0003 snapshot through its checkpoint and select the windows for its target session. New window creation is thereby visible without racing a list-then-listen sequence. RFC-0003 resource filters are exact and do not include children; resources=["session:s0"] is not a subscription to its windows or panes. The client uses the same event cursor/recovery rules on gaps and reconnect. It discovers current container names through ordinary scoped queries.

8. Attach boundary

COM-223 MUST support venomx attach --session <id> and venomx attach --pane <id>; exactly one selector is required. Session attachment initially shows the lowest-ID window, with local window choice and pane focus. An empty session/window MUST have a visible empty state. Creation or removal events update the shared topology; a removed focused pane falls back locally to the lowest-ID surviving pane. No focus change is broadcast as a mutation of another client’s view.

Each displayed pane MUST have one odd-ID stream on a negotiated binary connection with venomx.pane_stream, as RFC-0006 specifies. Several panes SHOULD share that connection. CONTROL or a separate ordinary connection carries discovery, events and verbs. There is no new frame, raw PTY subscription or alternate socket. A stream’s offset/decoder/resync state is independent of other streams; losing the connection requires restoring each affected stream. Emulating Vts MUST NOT send terminal query replies or publish duplicate OSC events.

Presentation clients follow RFC-0005’s owner-file default and managed-tree restrictions. All product mutations invoked by bindings MUST use the schema-defined daemon verbs and the same authorization as CLI/MCP calls. Local viewport actions remain part of COM-223: they MUST use typed named actions shared by command invocation and bindings, without sending scroll or selection state to the daemon. This RFC adds no product-local dispatch field to RFC-0004 format 1, whose product verbs remain daemon requests. Any shared generator extension required for those local actions must be specified and reviewed before COM-223 ships.

9. Implementation boundary and verification

COM-238 MUST use the COM-222 component, venom-std proto/identity/dispatcher, shared config/events/artifact bundles and process supervision. It MUST pass the packaged agent-contract conformance suite from COM-216; build-only or library tests are insufficient. COM-216’s prerequisites include the unfinished shared config (COM-210), events (COM-208), artifacts (COM-215) and supervisor (COM-211). Complete-schema identity is COM-226. This RFC can be reviewed while those dependencies progress; the implementation issue MUST record them and MUST NOT replace them with product-local stubs.

Foundation acceptance tests MUST cover:

  1. A real headless daemon and two socket clients create/discover a session, window and split; CLI/MCP share result, error and grant behavior.
  2. All clients disconnect; the same PTYs and panes remain discoverable, continue recording output and attach with restored screen state.
  3. Nested cols/rows splits and removal preserve tree invariants; empty windows/sessions survive until explicitly removed.
  4. Unknown callers cannot mutate; scoped callers see only authorized state; child principals and environments do not inherit owner tokens.
  5. Spawn/exec failures and injected allocation failures leave no visible resources, leaked children, PTYs, principals or creation events.
  6. Exit status/observation ordering and retained exited-pane capture work; live-pane/nonempty removal is rejected without side effects.
  7. Topology changes during event snapshot/replay cause neither missing windows nor duplicate panes; removal visibility respects grants.
  8. Startup locking, no-daemon exit 6, bounded SIGINT/SIGTERM shutdown and new source identity after restart follow the lifecycle above.
  9. Build, allocator/leak tests, formatting and shared conformance pass.
  10. argv0 (§5): argv[0] /usr/bin/nvim gives nvim, ./run.sh gives run.sh and -sh gives -sh; / and a/ give no argv0. A 300-byte argv[0] whose multi-byte code point straddles byte 255 is cut before that code point. The same value appears in pane.list, in pane.created data and in the pane entity. A restored archive pane has no argv0. RFC-0015 run --shell gives sh. Canaries placed in the directory part of argv[0] or in later argv elements appear in no pane.list result, event, entity, error or doctor output (terminal content the program itself prints is out of scope).
  11. process, pane.process_changed and title_current (§5, §7): sleep 1 in a shell pane changes process from the shell’s name to sleep and back, with one event each way, even with no PTY output. make running children stays make. A program that sets its title right after it starts gets its pane.process_changed before its pane.title_changed, and its title_current is true. The shell’s earlier title becomes not current on the next process change or command.started. A burst of output samples at most 4 times a second. An exited or archive pane has no process. A comm with invalid UTF-8 gives none.

COM-223’s separate end-to-end test adds two real presentation clients on one session: both see typing, their scroll/selection state is independent, one can be killed without affecting the other, and reattachment restores screen state. It additionally exercises incremental backfill, pending-row loading, offset mismatch, per-stream resync and topology changes.

Agent-contract impact

  • Verbs/schema: section 5 defines the minimum product manifest, merged with existing shared declarations; CLI, MCP, docs and completions come from that one schema. Presentation attach is the RFC-0006 local entry.
  • JSON/exits: RFC-0001/0002 unchanged; section 6 declares product errors.
  • Events: section 7 adds session/window events and snapshots to the RFC-0003 product catalog. Existing pane/OSC events retain their semantics.
  • Skill: generated text MUST describe explicit startup, in-memory lifetime, direct argv execution, named flags and read-only defaults. It MUST NOT expose the owner-token path or recommend agent attachment.
  • Doctor: add checks for source, session/window/pane counts, tree invariants, process supervision and RFC-0006 ring/client/resync state; preserve standard redaction and result-status rules.
  • Config: use shared ZON verbs and RFC-0005 grants plus RFC-0006 pane_stream fields. No new startup-mode or persistence config appears in this version. Reload MUST NOT destroy resources or change source.
  • Grants: section 5 supplies verb checks and section 3 the containment relation; RFC-0005 issuance, unknown defaults and audit rules are intact.
  • Headless/OSC: the daemon owns PTYs with no display. Only its Vt answers queries and publishes validated OSC 7501/133 observations.

Alternatives

  • Pane-only attach first: useful protocol validation, but cannot meet COM-223’s same-session criterion or provide normal product discovery.
  • Put the entire foundation in COM-223: mixes lifecycle and grant changes with renderer behavior and makes independent review difficult.
  • Build the entire MVP first: persistence, tmux compatibility, automatic startup, arbitrary layouts and trusted run/wait execution are useful later; they are not necessary to test session attachment.
  • Client-owned session topology: would let clients disagree about membership and make detached/headless mutations depend on a TUI.
  • One socket per pane: permissible but unnecessary; RFC-0006 already supplies independent streams and CONTROL traffic on one connection.
  • Assume same-UID clients are writers: contradicts RFC-0005’s explicit read-only unknown principal and managed-tree restrictions.
  • Do nothing: leaves COM-223 without a runnable product acceptance path.

Open questions

No unresolved decision is required for the scope proposed here. Maintainer review is required before acceptance, especially for the deliberately small initial surface: explicit persistent startup, equal splits, exited-only pane removal and no per-request cwd/environment overrides.

Local presentation-action schema support is deferred to COM-223 and MUST be specified and reviewed there before its client ships. Remaining future product extensions are tracked by COM-239; they MUST NOT be inferred from the simple create/send verbs or included in COM-238. Their implementing issues must specify any additional contracts before they ship.