ReferenceCLI
vx sentinel
Watch dispatched soldiers and record status changes
Watch dispatched soldiers and record status changes.
Usage:
vx sentinel Poll forever (foreground; run it backgrounded)
vx sentinel drain Print and acknowledge pending wakes right now
vx sentinel await Block until a wake arrives, or time out (for the
async Stop hook set up by 'vx init')
The sentinel polls every tracked "running" task's live herdr status,
across every project it's ever seen (one sentinel process per machine).
When one settles (done, blocked, unconfirmed or interrupted), it persists the change and records a
durable wake in the queue of the commander that dispatched the task. That is the task's own project
when the commander works there, and the commander's project when it dispatched from another
one (the task records its commander: project and herdr pane, set by 'vx dispatch').
"drain"/"await" resolve which project to act on from the
current directory (git toplevel, namespaced the same way "vx
dispatch" namespaces a project's camps) - not a repo, or a toplevel
that's itself a vexillum-managed camp, means nothing to drain here, so
both are silent no-ops rather than an error.
"drain" checks once, instantly: {"decision":"block",...} if something's
already pending for that project, {} otherwise. "await" is what the async
Stop hook actually calls - it blocks (re-checking every few seconds)
until a wake shows up or it times out, so the commander gets woken even
if its turn already ended before a soldier settled, not just when a wake
happens to already be pending at the exact moment a turn is ending.
"await" exits immediately, without waiting, outside a herdr-managed pane
(no HERDR_WORKSPACE_ID) - the hook is committed into the project and so
reaches any other tool's own Claude Code turns too, which have no task
for the sentinel to track.
A wake can also be forum feedback: the forum listener stores a user's prompt
in the project's inbox and records a forum wake, which both "drain" and
"await" report as "forum session <file>: N new messages
(ended: false). Run vx forum inbox." It is the same hook and the same
once-only delivery; a forum wake whose messages were already confirmed is
dropped. See "vx forum" for the listener.
A wake is consumed only once it is delivered. "drain" and "await" claim it,
print it, and only then remove it: when the hook that launched an "await" is
already gone (its parent process exited, it was signaled, or stderr cannot be
written), the wake is put back for the next one instead of being printed into
nothing, and a claim whose holder died is found again after two minutes.
Wakes waiting in a project with no live "await" and no live registered commander are
reported by 'vx doctor'. That is also how a Claude Code commander looks once its "await"
has run out its 55 minutes (inside the hook's one-hour timeout): it hears nothing until its next
turn, and vx status shows what happened meanwhile. A Pi commander starts a new "await"
by itself when one times out, and an OpenCode commander has none.
A project has one wake queue and a wake is delivered once, to whichever
commander asks first. Two commanders live in the same project (say Claude Code
and Pi) therefore do not both hear it: keep one commander per project (the
other can catch up with vx status and vx forum inbox, and vx doctor warns about the
setup).
An OpenCode 2.x commander has no Stop hook, so the sentinel itself delivers its
wakes: when a project has a live registered opencode commander (vx doctor reports it)
and no live await, the sentinel posts the same text into the commander's session
through OpenCode's HTTP API (opencode api POST /api/session/<id>/prompt, queued
behind the turn that is running, without touching the editor), once, under the
same rules for dropping what is no longer news. A wake it cannot deliver (no
session yet because nothing was sent in the pane, the service unreachable, a
sentinel with another HOME or XDG directories than the commander's OpenCode) is
put back and retried on every pass, never dropped. After 30 seconds of failing,
and only while herdr reports the commander idle, it types the wake into the pane
with "herdr agent prompt", which can glue the text onto a draft the person typed
but did not send.
One sentinel runs per machine, enforced by a lock the kernel drops when the
process dies, so a crash never leaves a stale claim. The sentinel records the
build it started from (version and executable file) in
~/.vexillum/sentinel.json and checks it after every pass: when that file has
been replaced on disk (a rebuild, an upgrade) it finishes the pass, releases
its lock and exits, so an old sentinel never keeps rewriting state with old
logic. "vx dispatch", "vx prompt" and every "await" start a fresh one from the
new binary; "vx doctor" warns about a sentinel that is stale or duplicated.
Each "await" records itself under ~/.vexillum/sentinel-awaiters/ and never
outlives the hook that launched it: it exits as soon as its parent process
is gone, and a newer "await" from the same session and project replaces the
previous turn's. Starting "vx sentinel" or any "await" also stops orphaned
ones left behind by a dead session. A process is only ever signaled after
it is verified to be a "vx sentinel await" - never by name.