Venom
RFC-0009venom-spec

On-demand daemon startup and last-session shutdown

Field Value
Status Accepted
Accepted 2026-11-06, explicitly approved by the maintainer
Authors commixon (commixon), with Codex
Created 2026-11-06
Linear COM-241; implementation COM-242
Supersedes None; opt-in extension to RFC-0007 §2
Superseded by None
Amended 2026-11-08 (venom-spec#92, normative, compatible addition; explicitly accepted by the maintainer on 2026-11-09): section 2 refuses automatic owner bootstrap below a manager

Summary

Add opt-in on-demand operation to venomx: an explicitly owner-authorized session create can start a missing daemon, and that daemon exits after its last session is explicitly removed. Explicit persistent operation remains the default. Empty sessions and sessions containing exited panes keep an on-demand daemon alive; clients and running-child counts do not control its lifetime. COM-242 implements this accepted design after the COM-238 foundation and shared bootstrap support COM-254. Acceptance ships no behavior and does not expand COM-238 or COM-223.

The governing contracts are RFC-0001, RFC-0002, RFC-0003, RFC-0004, RFC-0005 and RFC-0007. The accepted opt-in rules here qualify RFC-0007’s no-implicit-start requirement; all its persistent-mode behavior and resource/removal rules remain applicable.

Motivation

Persistent foreground operation is useful for supervision, debugging and headless services. Interactive users also need a mode in which creating their first session starts the service, detaching preserves work, and removing their last session releases the service. A connect-and-spawn loop alone can create competing servers, duplicate mutations, or silently give an agent owner rights. The startup policy, lock arbitration and lifetime transition must be specified together before COM-242 implements them.

Design

1. Examples and mode selection

Default operation remains explicit and persistent:

venomx serve

In another outside shell:

VENOMX_TOKEN=@owner venomx session create --name work --json

An existing daemon can persist the opt-in policy without changing its current lifetime:

VENOMX_TOKEN=@owner venomx config set --key daemon.startup --value '"on-demand"' --json
VENOMX_TOKEN=@owner venomx config reload --json

For a change from the default policy, both operations report restart_required=true until a new start applies the setting. A fresh installation can instead provision the following ZON through normal deployment tooling before any daemon exists:

.{
    .daemon = .{
        .startup = "on-demand",
        .startup_timeout_ms = 5000,
    },
}

With no daemon at the selected socket and that config installed:

$ VENOMX_TOKEN=@owner venomx session create --name work --json
{"session":"s0","name":"work"}
$ venomx session list --json
{"sessions":[{"session":"s0","name":"work"}]}
$ venomx attach --session s0

The last command requires COM-223 and a terminal; neither preceding command requires a TTY or stdin. Detach leaves s0 alive. For this empty session, explicit removal ends the on-demand daemon:

$ VENOMX_TOKEN=@owner venomx session remove --session s0 --json
{}
$ venomx ping --json
{"error":{"code":"unavailable.not_running","message":"No daemon at the selected socket","data":{"socket":"/run/user/1000/venom/venomx.sock"}}}

The path in the output is illustrative. No command silently switches sockets. Normal errors, including exit 6 for the last command, retain RFC-0002’s mapping.

Config lives at $XDG_CONFIG_HOME/venomx/config.zon, or $HOME/.config/venomx/config.zon if XDG_CONFIG_HOME is unset or empty. These roots MUST be absolute; an unavailable/relative root fails invalid_params.startup_config. The client and daemon MUST use the same shared config parser, defaults and validation. No instance-name or new config-path override is introduced here.

Setting Type / default Meaning
daemon.startup String: "explicit" (default) or "on-demand" Missing-daemon policy for eligible CLI calls
daemon.startup_timeout_ms Integer 100–60000, default 5000 Total connect/launch/readiness budget

serve always runs in the foreground. Its new optional string parameter lifetime has choices persistent and on-demand, default persistent. Thus serve remains persistent even with opt-in config; an on-demand foreground service is explicitly selected with:

venomx serve --lifetime on-demand --json

An auto-launcher executes the same binary’s serve --lifetime on-demand with the resolved socket explicitly supplied. It MUST NOT execute a PATH-selected replacement binary, invoke a shell, create a session as a startup side effect, or inherit a terminal as a controlling dependency. It MUST redirect stdin from the null device and keep daemon output out of the invoking command’s stdout/MCP channel. It MUST arrange process reaping and bound diagnostic capture; pipe closure or launcher exit MUST NOT terminate the daemon. No double-fork, service manager or platform API is mandated, provided those properties hold.

Both settings are restart-only active config. set persists without reload; reload validates atomically, reports the pending restart and MUST NOT change a running daemon’s lifetime, shutdown timer or source. Each new CLI invocation reads persisted startup policy only after an initial connect reports a missing daemon. A running daemon always wins: clients MUST NOT restart, replace or change its lifetime to match config. Invalid startup config MUST fail before launch, leaving files and resources unchanged. No config read is necessary to use an existing daemon.

2. Eligible commands and authority

Only the generated ordinary CLI invocation of session.create is eligible. It MUST pass local parameter validation, select on-demand policy, and have explicit VENOMX_TOKEN=@owner intent. Unknown callers, callers supplying no token, and callers supplying an ordinary token MUST NOT launch a missing daemon; they receive unavailable.not_running (6). Tokens are epoch-bound and MUST NOT be reused to bootstrap a new registry. The unavailable response makes no assertion about the absent daemon’s caller grants. A reachable daemon continues to return its authoritative denied.* (4) for unauthorized mutations.

The launcher MUST reject automatic owner bootstrap if either VENOMX_PRINCIPAL or VENOMX_PANE is present, even if empty, with denied.startup_context (4). It MUST NOT erase managed-context markers to make a caller eligible. These environment checks are conservative accident prevention, not a new identity boundary: they cannot authenticate an outside process or detect every escaped descendant. RFC-0005’s platform limitations still apply. Explicit local serve remains local user bootstrap authority, as in RFC-0001.

The launcher MUST also reject it, with the same error and before launching, when a manager’s marker (RFC-0005 section 3.8) matches a process above the caller. It checks its own process chain against the directory of the selected socket, as a daemon does at accept; a directory that doesn’t exist holds no marker. The new daemon would refuse this caller its owner token anyway, and would stay a child of that manager, so every pane in it would be ORPHAN on the manager’s daemon if something else created a session during the first-session grace. A supervisor of daemons holds no marker (RFC-0005 section 3.8), so it isn’t affected.

# As an agent of venom, with no venomx running and daemon.startup = "on-demand".
$ VENOMX_TOKEN=@owner venomx session create --json
{"error":{"code":"denied.startup_context","message":"..."}}

After readiness the CLI MUST use RFC-0005’s secure owner-file handling for the selected socket and perform normal hello authentication. The new daemon MUST authorize write on server:root before session.create. A launcher is not an authorization proxy. It MUST NOT forward its own identity on another caller’s behalf or hand out credentials. The child serve process MUST have inherited VENOMX_TOKEN/PRINCIPAL/PANE removed; owner credentials MUST NOT appear in argv, logs, readiness messages or pane environments. Owner-file creation, modes, secure reads and cleanup remain RFC-0005; they are not replaced by an environment-only shortcut.

All other invocations MUST NOT trigger startup: attach (either selector), window.create, pane.split, discovery/read verbs, ping, events, skill, doctor, all config and grant verbs, help and invalid commands. mcp startup and its upstream calls MUST NOT auto-start a daemon. An MCP adapter must not convert its own @owner context into the upstream caller’s authority. MCP session.create against an existing daemon still uses precisely the same schema, validation and authorization as CLI. Wire requests alone never start a daemon. No implicit session, window, shell, attach-or-create verb, or tmux compatibility shim is added.

3. Socket arbitration, readiness and deadlines

Resolve one socket using RFC-0002 precedence (--socket, VENOMX_SOCKET, default); resolve relative overrides to one absolute path before launch. The launcher MUST pass that exact path to the child and use it for every retry and owner-file lookup. Runtime/socket/lock/owner path permissions and symlink checks MUST remain in force. Permission, path-length, foreign-peer, protocol and auth errors MUST NOT trigger launch, unlink or fallback. Only initial ENOENT/ECONNREFUSED is eligible for startup arbitration.

The kernel flock on the RFC-0002 lock file is the sole daemon ownership lock. Every child serve follows RFC-0002 §2.5 unchanged. It alone, while holding that lock, may clean up a stale socket and owner file. Clients MUST NOT unlink socket/owner/lock paths. A stale PID printed in an unlocked lock file is informational and does not prevent startup. A held lock with no accepting socket means startup/shutdown is in progress, not staleness; no contender may kill its holder or remove its paths. Lock files MUST NOT be unlinked during normal cleanup, which would permit two locked inodes. Unsafe lock/owner files MUST fail closed under the shared transport and identity checks; a non-socket at the socket path remains conflict.not_a_socket (5).

Concurrent eligible commands MAY each launch at most one candidate serve process. Exactly one can acquire the lock; losers return the normal conflict.already_running internally. The CLI MUST absorb that launch race, then wait for the winner, rather than report a losing candidate’s conflict as the user’s mutation result. A direct explicit serve still reports conflict (5). No additional launcher lock or wire startup request is required. Waiters MUST use bounded, non-busy retries with backoff capped at 100 ms. A known held lock SHOULD be waited on before spawning a candidate.

One monotonic deadline starts at the first connection attempt and covers config reading, child launch, lock contention, connect and readiness. The budget is startup_timeout_ms; reading/validation time already spent MUST count against it. This is not the reserved global --timeout flag and does not set a remote handler execution deadline. If the initial connection succeeds, ordinary transport behavior applies instead.

Readiness requires all initialization (config, registry, owner file, subreaper, schema and listener) to succeed, followed by successful normal hello with owner auth and ping on the chosen connection. Listening or a PID alone is insufficient. The daemon MUST NOT dispatch ordinary requests before initialization completes. A winner that is already shutting down is not ready for creation. An explicit persistent serve winning the race is usable and stays persistent.

Before dispatch, a client MAY reconnect within its original deadline if the winner disappears or returns unavailable.shutdown. It MUST NOT launch more than its one candidate or loop indefinitely creating new daemons. After any byte of session.create is sent, it MUST NOT automatically resend the mutation, even after a timeout, lost reply or epoch change. A lost reply has unknown outcome and returns unavailable.startup_result (6); the caller must inspect state and decide what to do. Request IDs are connection-local, not idempotency keys. Concurrent independent invocations create one session per successful invocation; equal display names do not coalesce. “One intended creation” means no launcher-created session and no hidden retry of a caller’s mutation, not globally deduplicating users.

A known candidate exec/init failure returns unavailable.startup_failed (6), unless a winning daemon becomes ready within the remaining budget. If progress remains unresolved at the deadline, return unavailable.startup_deadline (6), consistent with RFC-0001’s startup unavailable rule, rather than the handler-deadline timeout (7). Cancellation returns cancelled (8). Neither a waiter nor a cancelling client may kill a winner: another client may already be using it. Abandoned zero-session on-demand starts clean themselves up as below. An auto-launched candidate MUST also bound its own initialization by the configured startup budget measured from process entry, aborting with cleanup if it cannot become ready. It MUST NOT wait indefinitely before starting the first-session grace. Config and diagnostic I/O MUST use bounded reads of regular files; a FIFO or device is not a config file. A kernel operation that cannot be interrupted is an operational failure, not a successful bounded-cleanup result.

4. Lifetime state machine and shutdown

Each daemon latches its lifetime from serve.lifetime. A persistent daemon never stops because of session, pane, child or client counts. An on-demand daemon has these states, serialized with mutations in its reactor:

initializing -> awaiting_first_session -> active -> stopping -> exited
                        |                    |
                        +---- deadline ------+ (active: last session removed)

awaiting_first_session permits a fixed 5000 ms monotonic grace from readiness for the first successful session creation. This grace is separate from each client’s startup budget, cannot be extended by probes or failed calls, and applies to foreground on-demand serve too. Successful creation transitions to active; a failure, denial or cancellation does not. Expiry with zero sessions transitions to stopping, even with clients connected. A creation/expiry race uses reactor commit order.

While active, only the number of session resources matters. An empty session, an empty window in a session, and retained exited panes all keep their session and therefore the daemon alive. Process exit MUST NOT remove panes/windows/sessions. Removing the last pane or window MUST NOT remove its ancestors. Removing a nonempty session still fails conflict.not_empty; removing running panes still fails as in RFC-0007. Clients must explicitly remove exited panes, empty windows, then the empty session. Disconnect, detach, terminal close and zero clients never start an exit timer while a session exists.

The successful removal of the final session MUST atomically commit the removal/event and latch stopping in the same reactor turn. There is no idle interval in which new mutations can revive it. If session.create commits first, the daemon stays active; if final removal commits first, authorized later mutations fail unavailable.shutdown (6) without effects. Authorization MUST precede this rejection so an unknown caller still receives denied.write. Already staged creations MUST either commit before the removal or be discarded without resources/events. New connections may be refused; existing reads MAY drain against the final consistent state. A client MUST NOT replay a rejected, already-sent create automatically.

The final removal response {} MUST be queued before connection closure. Shutdown MUST make a bounded attempt to flush it, without waiting forever for a slow reader. Losing that response is an unknown outcome, not a rollback. stopping is irreversible. The daemon MUST follow RFC-0007’s 2000 ms shutdown deadline, SIGHUP/SIGKILL/reaping and bounded socket drain, including SIGINT/SIGTERM while sessions exist. Last-session shutdown normally has no panes, but any pending prepared children MUST still be reaped. Stop acceptance, close streams/connections, clean up the socket and owner file while holding the lock, then release the lock last. Cleanup MUST NOT remove a successor’s files; lock ownership and the created file identities MUST be verified. Failures are operational failures reported to diagnostics; no success is claimed for leaked children. A new daemon has a fresh source and empty registry/state.

5. Schema, wire and errors

RFC-0004 format 1 remains closed and unchanged. The product config schema supplies the settings in §1. The shared bootstrap bundle MUST accept a compiled product-specific extension adding lifetime to local serve metadata before normalization/validation/digest calculation. It MUST NOT be a product override of serve, a new format-1 dispatch member or a remote tool. Other products need not expose the parameter. Its complete parameter record is:

.{
    .name = "lifetime",
    .summary = "Daemon lifetime policy",
    .type = .{ .kind = .string, .choices = .{ "persistent", "on-demand" } },
    .default = "persistent",
}

CLI eligibility is a compiled venomx bootstrap policy keyed to the existing normalized session.create declaration; it adds no parameter or handler. Generated help/docs/skill MUST include the same policy text from that bundle. No independently maintained CLI/MCP permission table is permitted. The shared bundle/runtime changes are tracked separately in venom-std COM-254, which is blocked by COM-241 and blocks COM-242. The session verb/result/grant and protocol version are unchanged. The ordinary exchange after startup is (credentials omitted; this trace uses the read-only implicit caller only for hello/ping):

{"id":1,"verb":"hello","params":{"proto":[1],"auth":{}}}
{"id":1,"ok":{"proto":1,"server":{"name":"venomx","version":"0.1.0"},"caps":[],"max_frame":1048576}}
{"id":2,"verb":"ping","params":{}}
{"id":2,"ok":{}}

On a separately owner-authenticated connection, creation is still:

{"id":3,"verb":"session.create","params":{"name":"work"}}
{"id":3,"ok":{"session":"s0","name":"work"}}

There is no wire flag requesting launch, elevation or lifetime changes. No readiness record or child serve --json output may enter CLI stdout. serve --json retains its sole termination {} or error wrapper behavior. The following additional bootstrap errors MUST be declared in generated bootstrap metadata, with empty data and the standard class exits:

Code Exit Meaning
invalid_params.startup_config 2 Invalid startup config or config location
denied.startup_context 4 Managed-context markers preclude automatic owner bootstrap
unavailable.startup_failed 6 Known launch/initialization failure
unavailable.startup_deadline 6 No ready daemon within the startup budget
unavailable.startup_result 6 Connection lost after create may have been received

Shared path/auth/lock/protocol errors and RFC-0007 handler errors retain their existing data shapes and exits. Bootstrap errors MUST redact tokens, raw environment and captured child logs; bounded human diagnostics go to stderr. Malformed CLI inputs fail validation (2) before any launch. Example deadline failure, exit 6:

{"error":{"code":"unavailable.startup_deadline","message":"Daemon did not become ready within the startup budget","data":{}}}

6. Events, diagnostics and conformance

No new log type is necessary. Startup does not manufacture session/pane events. Successful creation/removal emits the existing RFC-0007 events exactly once, in commit order. Failed/denied preparations emit no resource creation events; normal identity denial/audit rules still apply. Final session.removed is appended before stopping and attempted delivery is bounded by the shutdown deadline. Event delivery is not durable and MUST NOT be treated as guaranteed after shutdown. Streams end under the existing transport/cancellation rules. Restart changes source; old cursors use RFC-0003’s source_changed snapshot fallback on explicit reconnection. events MUST NOT auto-launch a replacement to satisfy a subscription.

doctor remains remote, read-only, with no launch or repair side effects. It MUST add stable checks daemon.lifetime, daemon.startup_policy, daemon.startup_deadline and daemon.lifecycle, reporting respectively latched lifetime, active/persisted policy with pending restart, configured budget, and state/session count/remaining first-session grace. Pending restart is warn; inconsistent lifecycle or cleanup/supervision failures are fail. Checks MUST use RFC-0001’s status/result policy, redaction and runnable credential-free fixes (venomx config check --json or venomx doctor --json for diagnostics). Before dispatch, a missing-daemon doctor failure MUST include error.data.fix="venomx serve"; it MUST NOT pretend a report came from a daemon. Other path errors retain appropriate quoted diagnostic fixes. Doctor MUST NOT expose token contents.

COM-242 MUST record evidence for this matrix using real CLI/socket clients, fault injection and the packaged agent-contract suite, including no-TTY runs. An implementation cannot substitute unit-only or build-only evidence.

Case Required observation
Default missing daemon session.create returns 6; no process/files/resources created
Opt-in valid owner creation One on-demand daemon and one requested empty session; no window/shell
Concurrent eligible commands Barrier-race N commands: one lock holder/source, N successful independent sessions, no extra bootstrap session; each readiness bounded
One command losing launch race Its child conflicts, winner used; one mutation sent and one session created
Lost create reply Return 6 unknown outcome, no automatic resend or second session
Ineligible/read/attach/MCP/help/invalid commands No launch; ordinary unavailable/usage behavior, help remains local
No token / ordinary old token No launch; no owner-file read or token promotion
Managed markers with @owner Exit 4, no launch; reachable bound/ORPHAN owner auth remains denied by RFC-0005
Manager’s marker above the caller, with @owner Exit 4 denied.startup_context, no launch and no process left behind
Reachable unknown caller Create denied (4), including while stopping; no mutation
Stale socket, unlocked stale-PID lock Only lock winner removes stale resources; one valid listener/owner file
Held lock, socket not ready No unlink/kill; wait succeeds or exit 6 within budget
Non-socket, insecure path, symlink/foreign owner file Fail closed with shared error; no replacement/fallback
Child exec/init failure Exit 6, bounded diagnostics; no leaked candidate, lock or owner file
Winner hangs / waiter cancelled Deadline 6 / cancellation 8; waiter does not kill winner; probes do not extend grace
Fresh on-demand zero sessions Exits after 5000 ms first-session grace plus bounded shutdown
Persistent zero sessions/clients/children Remains available, including explicit serve with on-demand config
Detach/crash all clients Session and PTYs remain alive; same source on reattach
Empty session / window / retained exited pane Keeps daemon alive; nonempty removal still conflicts
Remove last pane/window Ancestors retained, daemon remains alive
Remove final session vs create / initial grace vs create Commit order decides; no accepted creation lost, no revival after stopping
Final removal with slow/disconnected client State commits once; bounded flush/cleanup, unknown reply outcome allowed
Shutdown and restart Reap prepared/live children, drain within 2000 ms, remove only owned socket/owner files, release lock last; fresh source
Config set/reload Admin-only; invalid config atomic; restart-only pending values never change running lifetime
JSON/schema/skill/doctor/events Parse outputs, verify exits/digest/catalog/grants, no secrets/readiness chatter; source-change recovery

Agent-contract impact

  • Verbs/schema: adds local serve.lifetime through shared bootstrap metadata. Existing product wire verbs and grants are unchanged. Generated CLI/help/docs/completions/MCP use the complete schema; serve stays absent from MCP. Startup eligibility is documented as local CLI behavior.
  • JSON/exits: no new class or numeric exit; §5 declares refined errors. Existing result payloads and NDJSON framing remain unchanged.
  • Events: §6 preserves existing creation/removal events, source identity, snapshots, authorization and bounded stream termination. No lifecycle event claims durable completion of process shutdown.
  • Skill: generated text MUST explain default explicit operation, opt-in eligibility, no startup through MCP, unknown read-only access, retained sessions, unknown mutation outcomes and in-memory lifetime. It MUST NOT reveal owner paths or recommend owner elevation or TUI use to agents.
  • Doctor/config: §1 and §6 define restart-only settings, diagnosis and missing-daemon fixes. Agents use shared config verbs; installation-time provisioning is not an agent workaround for those verbs.
  • Grants: no new right; serve remains local bootstrap, session creation requires scoped write. Starting a service does not grant its caller any right. Explicit owner intent never bypasses daemon identity resolution.
  • Headless/OSC: startup works with closed stdin and no TTY/display. Detach preserves processes. OSC 7501/133 parsing, pane streams and query responses remain daemon-owned and convey no launch authority.

Alternatives

  • Do nothing: preserves foundation behavior but leaves COM-242 without an agreed startup/lifetime contract.
  • Auto-start on every command: makes read-only diagnostics and probes mutate local state and cannot bootstrap epoch-bound caller credentials.
  • Attach implicitly creates a session/shell: mixes presentation and mutations and would enlarge COM-223’s accepted scope.
  • Default on-demand operation: changes existing scripts/supervision; opt-in allows both modes while retaining the foreground serve path.
  • Exit on zero clients or zero children: terminates detached work or destroys retained terminal output and empty-session workflows.
  • Retry creates after disconnect / coalesce by name: request IDs are not durable idempotency keys and names are not unique (RFC-0007).
  • A launcher-created first session: duplicates command effects, hides grants and introduces a second mutation path.
  • Durable tokens or generic MCP bootstrap delegation: changes RFC-0005’s epoch and caller-identity model; unnecessary for explicit owner CLI use.

Open questions

All three proposals were explicitly accepted by the maintainer on 2026-11-06. No unresolved question remains within this RFC’s scope.

  1. Defaults and eligible surface — resolved: §1–2 specify explicit by default, config opt-in, and only owner-opted ordinary CLI session creation. MCP/attach bootstrap and implicit shells are excluded from COM-242.
  2. Bounded startup and initial emptiness — resolved: §3–4 specify a configurable 5000 ms client budget, fixed 5000 ms first-session grace, immediate irreversible stopping after final removal, and unavailable exit 6 for readiness failure. No generic timeout/idempotency mechanism is implied.
  3. Shared bootstrap metadata — resolved: §5 extends the shared bundle’s compiled construction API without opening format 1. COM-254 supplies the necessary shared-library change and explicitly blocks COM-242.

Durable restore/crash recovery and tmux compatibility remain outside this RFC and COM-242; COM-239 tracks the product-extension planning boundary. Revisiting cross-daemon/escaped-process identity and persistent credentials remains COM-235 under RFC-0005, not a startup-specific authorization scheme.