Venom
RFC-0003venom-spec

Event stream and OSC 7501 / OSC 133 interop

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

History

  • 2026-11-11 (venom-org/venom-spec#155, venom-org/venomx#179; compatible; explicitly accepted by the maintainer commixon on 2026-11-11): §8 also accepts kitty’s raw cmdline= form instead of ignoring it. libghostty-vt decodes both forms into one string and doesn’t say which one arrived. Both are untrusted display text, sanitized the same way. Shells should still send cmdline_url. §9’s case covers the raw form.
  • 2026-11-11 (venom-org/venom-spec#151, venom-org/venomx#178; compatible addition; explicitly accepted by the maintainer commixon on 2026-11-11): §8’s C marker may carry kitty’s cmdline_url extension. Its decoded text becomes an optional cmdline in command.started data and the command entity (§6). It is untrusted display text that the shell reports, so no command identity or trusted process data comes from it. §9 adds verification cases. Older readers ignore the field.

Summary

Define the shared events verb: a scoped NDJSON stream with ordered events, reconnection cursors, bounded retention and consistent snapshot recovery. venom-std implements the event log (COM-208); venom-term parses terminal observations and venomx publishes them. OSC 7501 and OSC 133 observations remain distinct from trusted agent requests and command execution results. The maintainer accepted this design and all four Open questions on 2026-11-06.

Motivation

An agent reconnecting after a lost socket needs to recover state without silently missing changes. A pane’s reported permission prompt must be visible without screen scraping, while never authorizing an operation. Every consumer needs the same meaning for event order, filters, resets and shell markers.

This completes the event surface reserved by RFC-0001, using RFC-0002 transport and RFC-0004 declarations. Raw PTY bytes, terminal snapshots and pane-stream offsets belong to RFC-0006 (COM-221). They are not event cursors or the state snapshots defined here.

Design

1. Examples first

After hello, subscribe to a pane’s command lifecycle (UUIDs illustrative):

{"id":7,"verb":"events","params":{"types":["command.*"],"resources":["pane:p3"],"after":"venomx/01234567-89ab-4cde-8fab-0123456789ab:40","follow":true}}
{"id":7,"item":{"seq":41,"ts":"2026-11-06T12:00:00.000Z","source":"venomx/01234567-89ab-4cde-8fab-0123456789ab","type":"command.started","data":{"pane":"p3","command":"c9"}}}
{"id":7,"item":{"seq":42,"ts":"2026-11-06T12:00:01.000Z","source":"venomx/01234567-89ab-4cde-8fab-0123456789ab","type":"command.finished","data":{"pane":"p3","command":"c9","exit_code":0,"reason":"osc133"}}}
{"id":7,"item":{"seq":42,"ts":"2026-11-06T12:00:01.000Z","source":"venomx/01234567-89ab-4cde-8fab-0123456789ab","type":"stream.checkpoint","data":{}}}

The CLI unwraps each item into one compact JSON line, with no final success line. Arrays are JSON flag values, not repeated flags or comma lists:

venomx events --types '["program.*","command.*"]' --resources '["pane:p3"]' --json
venomx events --after 'venomx/01234567-89ab-4cde-8fab-0123456789ab:42' --follow=false --json

Omitting after requests current state followed by live changes. A human renderer MAY abbreviate records; it MUST visibly distinguish snapshots, checkpoints and observed command/status events.

2. Envelope, ordering and identity

Every item MUST have exactly the following required members. Additional members introduced by future compatible revisions MUST be ignored by readers.

Member Type Meaning
seq Integer, 0–9007199254740991 Log position, or snapshot/checkpoint watermark
ts String UTC RFC 3339 timestamp with milliseconds, assigned by the daemon
source String <tool>/<epoch>; tool per RFC-0002, epoch a lowercase UUID v4
type String Dotted event name, using the RFC-0002 verb-name grammar
data Object Typed payload from the catalog below

A daemon MUST generate a new epoch whenever it starts, changes event-log identity, or would exhaust the safe integer range. No cursor survives such an epoch change; sequence numbers alone MUST NOT be used across restarts. A daemon has one ordered log across all its resources, starting at seq=1. Committed state transitions and their event append MUST be serialized as one operation, visible to snapshots together. Failure before commit MUST publish neither. This does not require a persistent database in v1.

Log events have strictly increasing, contiguous sequence numbers before retention. Wall time can move backwards; consumers MUST order by seq, not ts. Events from different sources have no total order. A forwarding daemon MUST NOT pretend to extend another daemon’s sequence. Cross-daemon aggregation is outside v1; clients can maintain one cursor per source.

A cursor is the literal concatenation <source>:<seq> using unsigned decimal without leading zeros except 0. The source has no colon. Consumers save the cursor only after applying the item. Delivery across reconnects is at least once; consumers MUST tolerate duplicate log events identified by (source, seq). Snapshots and checkpoints are control items, not log events; their watermarks can equal earlier items and MUST NOT be deduplicated as ordinary events. Watermarks MUST never decrease within a subscription.

3. Subscription and filters

The shared bundle declares events with grant=read, streams=true, result an empty object, the envelope as item, standard exit policy and these parameters:

Parameter RFC-0004 type Default / semantics
after String "": snapshot; otherwise exclusive cursor
types Array of strings []: all supported log types
resources Array of strings []: every resource in the caller’s read scope
follow Boolean true: continue after initial recovery; false: bounded catch-up

Each type selector MUST be an exact dotted name or a dotted prefix followed by .*; pane.* matches pane.created and pane.title_changed, not panes.created or pane. A syntactically valid unknown selector matches nothing. Invalid selectors fail with invalid_params.event_filter and data.param naming types or resources. Duplicate selectors are harmless. Resource selectors are exact <kind>:<id> strings; kind follows [a-z][a-z0-9_]*, id is a nonempty resource identifier with no colon or control characters. There are no resource globs. Resource IDs MUST NOT be reused within an epoch. Changing filters or caller scope requires a new snapshot (after=""), not reusing a cursor from the old projection.

Selectors within an array are ORed; the two arrays and authorization are ANDed. The server MUST apply filtering before delivery, without renumbering seq. Each catalog entry defines its resource. A resource selector does not implicitly include descendants. A client wanting all panes in a session must enumerate them or subscribe to its entire authorized scope.

A nonempty explicit resource list MUST be authorized in full before any items are sent, otherwise denied.event_scope (exit 4, empty data). Do not reveal whether an unauthorized resource exists. An authorized absent resource matches nothing. Empty resources mean the authorized subset, never global access. Filters cannot broaden a grant. The server MUST recheck scope for replay, snapshots and live delivery, redact secrets before logging, and end an affected subscription with denied.event_scope on scope change or grant expiry/revocation. RFC-0005 (COM-204) defines how identities and scopes are obtained; OSC fields MUST NOT supply them.

4. Replay, retention and checkpoints

On subscription the server MUST atomically capture high watermark H and register live delivery after H. With a valid cursor C, replay matching, authorized events with C < seq <= H, then send stream.checkpoint at H, then live events above H. Appends during replay MUST be queued or read from the log without a race; there MUST be no replay/live gap. If the required history is lost during replay, end with unavailable.event_gap (exit 6, empty data); the client restarts with a snapshot.

Let L be the oldest retained log position, or H+1 for an empty log. A same-epoch cursor in [L-1,H] is usable. C>H fails with invalid_params.event_cursor (exit 2, data.param="after"), as does malformed cursor syntax. An older cursor, or any well-formed cursor from a different source/epoch, MUST invoke the snapshot fallback in section 5 rather than silently starting at L. No-hello clients still use RFC-0002’s implicit version 1; no additional protocol feature capability is needed for events.

Default retention limits: retain a contiguous suffix, evicting oldest events when any limit is exceeded: 10 minutes by monotonic age, 10,000 events, or 16 MiB of compact envelope bytes. These are upper bounds, not a promised duration. The product MAY configure lower or higher bounds and MUST expose effective limits and current L, H, source and byte use through doctor. Trimming MUST NOT change current state. A single event exceeding the log byte budget forces eviction through its position; a future replay crosses a gap and falls back to a snapshot. Durability across process restarts is not required.

stream.checkpoint has empty data. It certifies that every matching event through its seq has been delivered, including when filters matched none. The server MUST send it at the replay/live boundary and at least every five seconds while following, using the processed high watermark. Control items bypass type filters. They contain no resource data, although global sequence numbers reveal aggregate activity. This metadata is part of the read grant.

With follow=false, complete immediately after the checkpoint with {"id":7,"ok":{}} (using the actual request id). An idle follow stream stays open. Cancellation, disconnect and slow-reader limits follow RFC-0002; Ctrl-C exits 8. No new acknowledgement verb, binary frame or credit protocol is introduced. A server MUST never silently skip a matching live event under backpressure: close the connection or return unavailable.event_gap.

5. Snapshot fallback

A state snapshot replaces the consumer’s entire selected projection; it is not a reconstruction of discarded historical commands or notifications. The server MUST capture a consistent read view at H and send one or more state.snapshot items, all at seq=H, before events above H. Their ts is the capture time. data has:

Field Type Meaning
reason String initial, expired or source_changed
entities Array of objects Each has required string resource, entity_type, content
complete Boolean True only on the last chunk

content contains a compact JSON document for one entity. This is the same string-container technique as RFC-0001’s config schema: format 1 of RFC-0004 has no arbitrary maps or discriminated unions. Products MUST document and validate the JSON document’s shape. The snapshot MUST include current entities associated with selected event types, within selected resources and scope, even when those entities have no recently retained events. Historical-only events contribute no entities. Each entity occurs once; empty projections still send one chunk with entities=[], complete=true. Clients MUST stage chunks, replace state and save the cursor only when complete=true; a failed/incomplete snapshot MUST be discarded.

Chunks MUST fit the negotiated RFC-0002 max_frame, including the enclosing item or CONTROL message. If one entity cannot fit, fail with unsupported.event_size (exit 10, empty data). Products SHOULD keep entity projections small; bulk terminal state is RFC-0006. Snapshot read views and live queues MUST be bounded (recommended combined budget: 8 MiB per request); exhaustion ends with unavailable.event_gap, never a partial success. Retry with narrower resources or a larger negotiated frame limit where applicable.

After the final chunk, send a checkpoint at H, then either complete or follow. A snapshot at H=0 is valid. For example, an expired cursor and an empty selected projection produce these wire items before live delivery:

{"id":7,"item":{"seq":90,"ts":"2026-11-06T12:05:00.000Z","source":"venomx/01234567-89ab-4cde-8fab-0123456789ab","type":"state.snapshot","data":{"reason":"expired","entities":[],"complete":true}}}
{"id":7,"item":{"seq":90,"ts":"2026-11-06T12:05:00.000Z","source":"venomx/01234567-89ab-4cde-8fab-0123456789ab","type":"stream.checkpoint","data":{}}}

6. v1 event catalog and schema representation

All fields listed below are required unless marked optional. Payload fields not applicable to an event MUST be absent. Log events are emitted after the named transition commits; repeated equivalent changes produce no event. Each product MUST declare which applicable types it implements. The two control types are mandatory even for a daemon with no product events.

Type Resource data fields Snapshot entity
stream.checkpoint Subscription Empty None; section 4
state.snapshot Subscription reason, entities, complete Section 5
config.changed config:root restart_required boolean config: redacted persisted ZON in string content
config.reloaded config:root restart_required boolean config: same entity, also active redacted ZON in string active_content
pane.created pane:<pane> pane string, session string pane: pane, session, title, exited boolean, optional exit_code
pane.title_changed pane:<pane> pane, title strings pane
pane.exited pane:<pane> pane, optional integer exit_code pane (retained until removal)
pane.removed pane:<pane> pane pane disappears
program.status_changed pane:<pane> pane, reason, records array program_status: pane, records
command.started pane:<pane> pane, command strings, optional cmdline string command: active pane, command, optional cmdline
command.finished pane:<pane> pane, command, reason, optional exit_code command disappears; no historical snapshot

Snapshot resource is the catalog resource. entity_type is the name above; content is its JSON object. Config snapshots always include both persisted and active content plus restart_required; selecting either config event selects that entity. No config secrets or mutation values appear in events. Exit codes, when known, are integers 0–255; absence means unknown and MUST NOT be treated as zero. Pane exit status comes from the child wait status, not OSC. Products MUST document signal-to-status conversion.

The shared schema MUST describe the envelope as a closed object with required seq, ts, source, type, data. Its data descriptor is a closed object containing the union of the catalog’s field names, each optional with its single fixed type. For example pane is always a string and records always an array of the record object below. This is expressible with RFC-0004 format 1, without adding a union type. type is a string without choices, allowing future event names; the publisher MUST additionally validate the per-type required/forbidden fields from its catalog. The generated documentation MUST include that catalog, not imply every combination is meaningful.

If a log item cannot fit the negotiated frame limit, terminate that subscription with unsupported.event_size; never truncate or skip it. Readers validate transport framing but MUST NOT reject future extra fields merely because their compiled catalog is older. The closed schema describes what the current publisher emits, not a ban on compatible future additions.

Product additions MUST extend that product’s item descriptor and catalog at build time with nonconflicting field types and document snapshot behavior. Consumers MUST ignore unknown event types and fields while advancing their cursor. Renaming/removing a field or changing its meaning requires a protocol major revision; compatible additions do not. New product event semantics must be specified before implementation; this catalog does not invent turn, inbox, grant or pane-stream events for future RFCs.

7. OSC 7501 ingestion and event mapping

The interoperability baseline is the Program Status Protocol revision 0.2 (2026-10-06), published in Rex’s “Program Status Protocol” documentation; tuios’s “Program Status” documentation identifies the same revision. The parser and record store MUST implement that revision, including validation, limits, inheritance, replacement, subtree clearing and lifetime rules. The upstream document is incorporated by reference; later revisions require explicit review here. Do not substitute window-title heuristics.

Wire example (POSIX shell, prints bytes to stdout for a PTY fixture):

printf '\033]7501;state=blocked:kind=permission:app=build:msg=QXBwcm92ZT8=\033\\'
printf '\033]7501;state=working:app=build:progress=40\007'
printf '\033]7501;state=clear\033\\'

Venom’s records is the full current normalized set, sorted by id; the root is id="". A record has required string id, string state (idle, working, done, blocked, error), and optional string kind, app, title, msg, and optional integer progress (0–100). Store the explicitly reported app; consumers compute inherited display app from ancestors. Decoded text is plain text. Absence MUST remain absence, not an empty string or zero. A clear report is a removal, never a record with state clear.

Publish exactly one program.status_changed per effective record-set transition, including clearing, eviction and lifecycle cleanup. reason is report, clear, evicted, prompt, process_exit, pane_removed or reset; a report that also evicts uses evicted. The event contains the resulting whole set, so consumers replace it atomically, including records=[]. Identical reports may refresh store recency but do not emit an unchanged projection.

The first fixture, applied to an empty pane, produces this projection (sequence and command ids are assigned by the daemon):

{"seq":43,"ts":"2026-11-06T12:00:02.000Z","source":"venomx/01234567-89ab-4cde-8fab-0123456789ab","type":"program.status_changed","data":{"pane":"p3","reason":"report","records":[{"id":"","state":"blocked","kind":"permission","app":"build","msg":"Approve?"}]}}

For presentation, the summary priority is blocked > error > done > working > idle, then lexical id. This follows tuios’s urgency order; the event preserves all records so consumers can choose another presentation. Venom retains idle records on prompt/process exit and done/error until explicit clear, reset or pane removal; no visibility or keyboard acknowledgement policy is imposed. Reset is RIS, not DECSTR. Before pane.removed, clear remaining records with reason pane_removed. Pane removal MUST follow supervised exit and the events in section 8. OSC 133 A triggers upstream prompt cleanup. Observable exit of the process attached to the PTY triggers exit cleanup; do not guess exits from silence.

Queries (7501;?) MUST produce only the upstream feature-detection reply on the PTY input path, using the query’s terminator; they emit no event. The single authoritative parser runs before raw-byte fan-out, so multiple attached clients MUST NOT each inject replies or duplicate events. Parsing MUST tolerate every byte split, bounded incomplete sequences and recovery after malformed input. Invalid reports produce no state change or event. Diagnostic counters MAY increase, without logging raw messages or secrets.

A blocked permission/question/auth record is an observation. It MUST NOT create an approval token, grant rights, answer a prompt, invoke a verb or prove the identity named by app. Any later inbox integration needs a trusted adapter and RFC-0005 authorization; auth text MUST NOT elicit stored secrets. Other clients’ PTY bytes and terminal rendering remain governed by RFC-0006.

8. OSC 133 command lifecycle

Baseline marker meanings are those documented by Microsoft VS Code’s “Terminal Shell Integration” and kitty’s “Shell integration”: A begins a prompt, B ends the prompt, C begins execution/output, D ends execution with an optional decimal exit code. Accept BEL and ESC-backslash terminators. The mapping to Venom events is defined here, independently of shell tooling.

Maintain at most one observed active command per pane:

Input Venom transition
A Finish an active command with reason prompt, unknown status; perform OSC 7501 prompt cleanup
B Mark input region only; no command event
C, optional cmdline_url= If none active, allocate epoch-unique command id and emit command.started, with cmdline when given (below); duplicate C while active is ignored
D, optional ;0–;255 If active, emit command.finished with reason osc133 and optional exit_code; clear active command
Pane exit Finish active command with reason pane_exit, unknown command status, before pane.exited
RIS Finish active command with reason reset, unknown status, then reset observation state

D without an active command and repeated A/B/D produce no command event. Malformed or out-of-range D status discards the marker and leaves state unchanged. D with no status finishes with unknown status. A/B/C accept semicolon extension fields, ignored in v1 except C’s cmdline_url; D accepts one optional status and subsequent extension fields. Unknown marker letters are ignored. DECSTR and alternate-screen switches MUST NOT invent command completions.

A’s synthetic command finish precedes its program-status cleanup event. On pane exit, command finish precedes program cleanup, which precedes pane.exited. Command ids are generated by the daemon, never taken from PTY text. OSC 133 supplies no cwd, trusted process identity or authoritative wait result, and command text only as the untrusted cmdline below. In particular, an observed D with status 0 MUST NOT complete a trusted run --wait; that operation uses its own child supervision. OSC 7501 reports do not synthesize command events, and command markers do not synthesize program done/error reports.

cmdline. C MAY carry kitty’s cmdline_url=<text> extension field: the command line the shell is about to run, percent-encoded. It MAY instead carry kitty’s raw cmdline=<text> form, which kitty’s shell integration quotes with backslashes. The daemon decodes either one (%XX escapes for cmdline_url, kitty’s unquoting for cmdline), then replaces each C0 or C1 control and DEL with U+FFFD, and cuts the result to at most 1024 bytes on a UTF-8 code-point boundary. If the decoded text is not valid UTF-8 or is empty, there is no cmdline. A malformed escape makes only cmdline absent; the C marker still starts the command. Shells SHOULD send cmdline_url, which carries any byte safely. cmdline is what the shell says, not what runs: an alias, a function or a lie are all possible. So it is display text only. It is never a command id, a trusted process identity or input to a grant, and clients render it as untrusted text. It is set once, at C, and command.finished doesn’t repeat it.

printf '\033]133;C;cmdline_url=ping%%20google.com\033\\'

That starts a command whose cmdline is ping google.com.

A complete successful command fixture is:

printf '\033]133;A\033\\prompt> \033]133;B\033\\true\r\n\033]133;C\033\\\033]133;D;0\033\\'

It emits one command.started and one command.finished with the same allocated command id, exit code 0 and reason osc133. Partial command history from before parser startup is not invented.

9. Verification cases for downstream conformance

COM-208 and the OSC-consuming product MUST exercise these cases; this RFC supplies fixtures and expected semantics, not a claimed implementation test:

  • Append while replay/snapshot capture is occurring; verify exactly the captured projection then every later matching event, in sequence order.
  • Replay at L-1, L, H, before L-1, ahead of H, and from another epoch. Verify snapshot reasons, empty logs and multi-chunk commit behavior.
  • Filter out every event while advancing H; save a checkpoint and reconnect without replaying already processed unrelated history. Change filters only after resetting from a snapshot.
  • Revoke read scope during replay and snapshot; disclose no subsequent items. Verify denied explicit selectors and redacted snapshot content.
  • Exhaust retention during replay and overflow a live queue; verify explicit failure/disconnect and snapshot recovery, never a silent gap.
  • Feed each OSC fixture one byte at a time and at every split boundary; BEL and ESC-backslash variants MUST agree. Verify no raw controls enter NDJSON.
  • Try unknown states/keys, repeated keys, invalid base64, decoded controls, oversize sequences, bad ids, nested clear, full reset, soft reset and record-cap eviction against the pinned upstream OSC 7501 rules.
  • Query OSC 7501 with two clients attached; observe one reply and zero events.
  • Exercise all three blocked kinds, missing kind, duplicate reports and a working child under an idle root. Verify prompt/exit lifetimes and that no grant or approval is created.
  • Exercise duplicate C, orphan D, D without status, malformed D, A during an active command and pane exit during a command. Check the ordering above.
  • Send C with cmdline_url that is plain, percent-encoded with multi-byte UTF-8, holds encoded controls, has a malformed escape, decodes to invalid UTF-8, or is over 1024 bytes. Also send kitty’s raw cmdline= form, plain and with backslash-quoted spaces. Check the cmdline that results (or its absence) and that the command starts every time.
  • Cancel the CLI stream and overrun MCP’s bounded buffer; verify RFC-0002 exit 8 and RFC-0004 unsupported.stream_limit, respectively.

Agent-contract impact

  • Verbs/schema: fixes events signature and fixed-type payload field superset; shared handlers validate the per-event catalog. No extra action or alternate privileged TUI route.
  • JSON/exits: ordinary RFC-0002 stream mapping and error classes; successful bounded catch-up exits 0 without an extra stdout record.
  • Events: v1 catalog above; terminal observations are explicitly untrusted.
  • Skill/doctor/config: generated skill MUST explain cursor resets, filters and checkpoints. Doctor reports retention limits and watermarks. Retention overrides, if offered, use existing ZON config verbs; this RFC does not add product-specific config keys. Other shared signatures are unchanged.
  • Grants: read plus resource scope throughout the subscription. No write, approval, auth or secret access inferred from terminal bytes.
  • Headless/OSC: subscription needs no TTY. PTY consumers implement the pinned OSC profile; non-PTY daemons need only the event surface.
  • MCP: use RFC-0004 bounded buffering unchanged; recommend follow=false for catch-up. Large snapshots may exceed its 1024-item/1-MiB limit and MUST fail rather than truncate. Continuous subscriptions use the CLI/wire.

Alternatives

  • Only live delivery / do nothing: reconnect silently loses state changes.
  • Sequence reset without an epoch: a cursor can accidentally reference a different daemon incarnation. Epoch-qualified source avoids that ambiguity.
  • Replay forever: unnecessary storage and secret-retention cost; current state plus bounded history supports read-model recovery.
  • Error only for old cursors: every client would reinvent snapshot/race handling. A typed snapshot makes recovery part of the subscription.
  • Arbitrary JSON payload type in RFC-0004: would change the accepted format. A fixed field superset and JSON-string snapshot documents fit format 1; richer schema types can be proposed separately if this becomes cumbersome.
  • Screen scraping or OSC as approval authority: unstable and forgeable. Observe protocol records; authorize trusted verbs separately.

Open questions

All four questions were resolved on 2026-11-06: the maintainer accepted the proposed answers and explicitly accepted this RFC.

  1. Recovery policy — resolved: automatic chunked snapshots and synthetic checkpoints, with a fresh epoch on every daemon start. Scope or filter changes require a fresh snapshot. Cursor durability across restart is outside v1.
  2. Retention and resource bounds — resolved: defaults are 10 minutes / 10,000 events / 16 MiB log, five-second checkpoints and an 8-MiB snapshot/queue budget, as specified in sections 4 and 5.
  3. Schema representation — resolved: a fixed-type field superset for data and string-contained JSON snapshot documents fit RFC-0004 format 1. This RFC does not extend that format’s type system.
  4. OSC lifecycle/presentation — resolved: pin upstream revision 0.2, retain idle records, clear done/error only through the explicit lifecycle rules in section 7, and use the specified urgency summary and incomplete OSC 133 recovery policy. Foreground-process-group attribution beyond the attached PTY process remains outside v1.