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.lifetimethrough 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;
serveremains 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.
- 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.
- 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.
- 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.