DocsSupervision (zirv ctx)
Sending mail between sessions
Agent sessions running on the same machine can leave each other short notes,
scoped to the current repository, with zirv ctx send and zirv ctx inbox:
zirv ctx send --message "the webhook route moved to /v2/webhook"
zirv ctx inboxzirv ctx status reports how many are waiting (mail: N unread). A mail
message is free-form text written by whichever agent session sent it, not an
operator instruction — see Trust boundary for how it's
capped and labeled the same way the other untrusted surfaces are.
Add --to-session <prefix> to address one specific live session instead of
every session an agent has: zirv ctx send --to-session abcd1234 --message "..." resolves abcd1234 (a short id, or a unique prefix of one — see
Session registry and nudging below) against
the live registry and stores the full resolved id, so the message keeps
finding its target even if the registry record itself is gone by the time it
is read. A session-addressed message is only ever delivered into a
headless session's own launch prompt (exec/loop, when
[mail] enabled = true); an interactive session (chat/wrap) only ever
gets a one-line unread-count advisory on its status bar or event channel,
split as broadcast+direct once something is addressed to it specifically —
never the message body itself, the same "advisory, not authority" rule
zirv ctx nudge follows below. There is no environment variable for
--to-session: unlike the config knobs elsewhere in this document, session
addressing is a per-invocation argument, not something an operator or a repo
would want to pin as a default.
Mid-turn delivery (Claude only, off by default). With [mail] mid_turn = true
(ZIRV_CTX_MAIL_MID_TURN, operator-only), Claude's PostToolUse hook returns
mail addressed to this exact session (--to-session, including root-level
mail filed in the mailbox it registered from) as additionalContext, merged
beside any compaction or checkpoint note. Each message is framed as
agent-authored (sender named, no user authority, information not
instruction), rendered with the same envelope zirv ctx inbox prints, and
moved to read/ as it is delivered, so the idle-boundary advisory and later
hooks never repeat it. At most 4 KiB is injected per call; a message that
does not fit stays unread and is named once by a N more: zirv ctx inbox
pointer. With the key off the hook does no mail work; with it on and no
ZIRV_CTX_SESSION the hook does none either, and with an empty inbox the
output is byte-identical to the key-off output. Codex is unchanged.
A body over [mail] max_message_bytes/max_delivered_bytes is still stored,
truncated, rather than failing the send — but the cut text is no longer just
lost. The full original body is written to a sidecar file under the
mailbox's own full/ subdirectory (capped at 1 MiB of its own, with a
trailing [truncated] marker if even that is exceeded), and the stored
message's own [truncated] marker becomes [truncated; full body: <path>],
naming that sidecar. full/ is a dedicated directory nothing else ever
moves a message into or out of — unlike the message itself, a sidecar is
never relocated into read/ on consume or into a dead-letter directory once
its TTL expires, so a marker's path always stays valid for as long as the
sidecar exists. It is pruned to the same [mail] keep as the mailbox
itself, in its own pass, so it does not accumulate forever either.
A delegated worker's own final report is persisted before any report-back
mail is sent, at
<state>/logs/delegation-results/<session>.json (capped at 1 MiB, with a
report_truncated flag when cut) — whether or not the delegation declared a
--result-schema/--result-kind contract. The report-back mail for a
contract-declared delegation names that file as a trailing full report: <path> line, and zirv ctx agent --json's own receipt carries it as
result_path (see Delegation receipt above).
Session registry and nudging#
Every supervised session (wrap, exec, loop, chat) registers itself
under the state dir at <state>/sessions/<short8>.json for as long as it is
alive — best effort, released when the supervisor exits, and swept
automatically the moment zirv ctx status (or anything else that reads the
registry) notices its process is gone. zirv ctx status reports it under
sessions:, one line per record:
sessions:
abcdef12 claude exec pid 48213 3m live -work-my-repo
9a8b7c6d claude wrap pid 19042 40m stale -work-my-repo
1f2e3d4c claude wrap pid 51120 5m unreachable -work-other-repounreachable means the process is running but bound no turn-signal socket
(--no-supervise, or the socket failed to bind), so it never checks for
wake-ups: zirv ctx nudge refuses such a target and says so, while
zirv ctx send still leaves a message for its next run.
<short> is the same eight-character id --to-session and zirv ctx nudge
resolve a prefix against. A socket left behind by an older zirv binary that
predates the registry (s/*.sock with no matching JSON record) still shows
up, labeled (no record), so a mixed-version machine never silently drops a
live session from the listing.
Addressing a parked seat (see "the displaced harness is parked, not
closed" below) whose own supervisor process has since exited -- a "ghost
park" -- is recognized instead of reported as a plain unknown id. If its
park window has already elapsed, send/nudge resume it in place before
delivering, clearing the park without ever opening a handover onto a
different harness -- that stays wrap's/dash's own supervision loop, never a
short-lived send/nudge invocation's; if the window has not yet elapsed,
the mail is queued as usual and the reply names the park and its reason
instead of a bare not-found. Either way mail never resumes a park early on
its own -- only a window that has already elapsed does. A ghost-parked
seat's own repository cannot be recovered from its seat record alone, so its
mail files under the sender's own repo mailbox, mirroring the undirected
--claim-once fallback.
zirv ctx nudge <prefix> --message <text> wakes a live session early instead
of waiting for it to notice on its own:
zirv ctx nudge abcd --message "please check the new failing test"A nudge prefix must be at least four characters (or a session's whole short
id) — unlike --to-session, which only addresses a message, a nudge wakes
and can restart what it resolves to, and on a machine running one session a
single mistyped character is still "unique". A shorter prefix is refused and
the live sessions are named back to you.
The message itself is ordinary, durable mail (visible in zirv ctx inbox
even if the wake-up is missed), so the two pieces are decoupled on purpose: a
nudge is a wake-up signal plus a payload, stored separately, and losing the
wake-up never loses the message. For a headless session (exec), a nudge
costs the in-flight turn — the session is stopped and relaunched with a
handoff distilled from the transcript so far, the same recovery path a rot
restart uses, just triggered by an operator instead of the rot engine. That
restart is bounded by [supervise] max_nudges (default 3, ZIRV_CTX_MAX_NUDGES)
so a session cannot be interrupted indefinitely; past the cap a nudge's
message is still queued as mail but the session runs on untouched. The cap
counts consecutive nudges — it resets as soon as the session reports a turn
of its own, so a long-running session that keeps making progress can keep
being steered. For an interactive session (wrap or chat), a nudge is
advisory only: it never restarts or types anything into the agent, and it
never receives the message body — it just surfaces on the status bar and
event channel that a nudge arrived, pointing at zirv ctx inbox. zirv ctx nudge says so at send time too when the target it resolved is interactive,
so an advisory delivery never looks like a nudge that silently did nothing. Either way, latency is bounded by
[supervise] poll_ms (default 2000ms) — the interval a supervisor's own tick
already runs on, since a nudge just claims a marker file that same tick
checks for.
Banner, status bar and events#
A zirv ctx chat session (bare zirv included) with a real, large-enough
terminal attached also gets a bit of chrome:
- a one-time launch banner naming the resolved harness, the rule that chose it, and the session id;
- a reserved one-row status bar pinned to the bottom of the terminal;
- an event channel on stderr, one line per notable event, in the shape
[HH:MM:SS] zirv ▸ <message>.
All three degrade together and only in one direction: --simple,
--no-supervise, a terminal narrower than 40 columns or shorter than 8
rows, or a non-terminal stdout turns every piece off, and nothing here ever
upgrades a session mid-run. Turn just the event channel off with --quiet,
the ZIRV_CTX_QUIET environment variable, or [chrome] events = false in
ctx.toml; [chrome] banner and [chrome] bar switch the other two off
the same way. See .settings.toml below for enabling or
disabling the harnesses themselves (claude, codex) — a separate file from
[chrome].