saphan serverd
Run the metering daemon: lifecycle skeleton + manual trigger (order serverd-w1 P2)
saphan serverd
Run the metering daemon: lifecycle skeleton + manual trigger (order serverd-w1 P2)
saphan serverd is the metering daemon — a new sibling of "saphan server", proving the process out on the same lifecycle skeleton (pidfile + signals) before P4 adds a monitor loop on top. P2 carries ZERO monitor logic: the foreground loop is lifecycle only, plus one manual trigger an operator can fire by hand to smoke-test the metering path end to end.
Subcommands
| Subcommand | What it does |
|---|---|
saphan serverd mailbox | Drop an owner directive into the mailbox — INSERT only, zero dispatch (order serverd-w1 P5) |
saphan serverd pause | Hold a stream's automatic spawns until a named instant — zero cost, and it ends by itself |
saphan serverd reload | Re-read the daemon's CONFIG (SIGHUP) — P2 has no monitor state to lose |
saphan serverd start | Start the metering daemon (foreground by default; --detach to background it) |
saphan serverd status | Report whether the metering daemon is running |
saphan serverd stop | Stop the running daemon (SIGTERM) |
saphan serverd trigger | Signal the running daemon to perform ONE spawn through the shared door (manual smoke trigger) |
saphan serverd mailbox
Drop an owner directive into the mailbox — INSERT only, zero dispatch (order serverd-w1 P5)
mailbox writes ONE row to the store's mailbox table: stream (the FIRST-CLASS addressee — PRAWO ADRESATA, founding incident relay->scribe, 2026-07-18) and directive_text (UNSIGNED in W1, Plan §2.4 — a landing pad, not the full owner-signed/routed DIRECTIVE_AUTHORING_ROADMAP chain).
This verb never dispatches anything — delivery is exclusively the running daemon's monitor tick, third trigger ("mailbox"): the next tick that finds a standing rule with trigger=mailbox matching THIS stream picks the row up and resumes (WakeMessage = directive_text) or spawns fresh with the directive composed into the kickoff brief. A directive addressed to a stream with no matching rule in the standing config stays undelivered and VISIBLE in the daemon's tick log every tick — never a silent drop, and never re-routed to another stream (owner re-addressing is DIRECTIVE_AUTHORING_ROADMAP, W2+, out of W1 scope).
Subcommands
| Subcommand | What it does |
|---|---|
saphan serverd mailbox list | Show mailbox directives and their state — pending | delivered | retracted | exhausted |
saphan serverd mailbox retract | Withdraw a PENDING mailbox directive — its own state, never delivered=1 (order mailbox-spawns-fresh-and-retracts §3) |
Options
| Option | Type | Default | Description |
|---|---|---|---|
--directive | string | unset | directive text, UNSIGNED in W1 (Plan §2.4); required |
--stream | string | unset | addressee stream — first-class (PRAWO ADRESATA); required |
--urgent | bool | false | cross any pause window in force on this stream instead of waiting behind it (order pausing-a-stream-must-not-look-like-killing-it §3) |
saphan serverd mailbox list
Show mailbox directives and their state — pending | delivered | retracted | exhausted
list renders every mailbox row, oldest first, in ONE of four renderings — and each rendering carries a field the other three structurally cannot, so the states can never be told apart by eye alone but agree in the record:
pending carries neither a run nor a withdrawal (and attempts=/attempt_id=/
attempt_status= once it has failed at least once, without becoming
any other state)
delivered carries run=<run_id>, the run it rode in on
retracted carries by=<actor> and ground=<why>, and NO run
exhausted carries attempts=<n>/<cap>, attempt_id= and attempt_status=, and
NO run and NO withdrawal — a directive that died repeatedly
without ever being delivered or retracted, and stopped being
retried at the cap (migration 43)
A fifth rendering, CONFLATED, exists and is unreachable through the engine's own writers: a row carrying BOTH delivered and retracted is a contradiction some hand wrote directly into the store, and it is shown as the contradiction it is rather than resolved in favour of whichever column a reader tested first.
Options
| Option | Type | Default | Description |
|---|---|---|---|
--stream | string | unset | show only directives addressed to this stream |
saphan serverd mailbox retract
Withdraw a PENDING mailbox directive — its own state, never delivered=1 (order mailbox-spawns-fresh-and-retracts §3)
retract marks one PENDING mailbox row as WITHDRAWN: nobody was meant to read it. This is NOT delivery. delivered=1 means "handed to a reader" and is coupled to the run it rode in on; retraction gets its own column precisely so a reader can tell the two apart, and so the delivered column never has to carry two incompatible facts.
--actor and --ground are both MANDATORY and both refused empty: a row withdrawn without a recorded reason is the same silence one layer over. Any HUMAN may retract (the master watching a stream is usually the one who knows a premise expired, not the owner) — the engine may not, refused by the same test that refuses it as the actor of a gate decision.
REFUSED on a DELIVERED row: a directive handed to a reader cannot be un-handed. REFUSED on an already-retracted row: a correction is a NEW directive, never an edit of this one — the record is append-in-meaning, and the first withdrawal's actor and ground stay exactly as they were written.
Options
| Option | Type | Default | Description |
|---|---|---|---|
--actor | string | unset | the HUMAN withdrawing it — never the engine; required |
--ground | string | unset | why the directive is no longer to be delivered; required |
--id | int64 | 0 | mailbox row id to withdraw; required |
saphan serverd pause
Hold a stream's automatic spawns until a named instant — zero cost, and it ends by itself
pause is a predicate at the spawn door, not a kill switch and not a signal to a running process. Between turns a headless leg does not exist as a process, so holding a stream back is simply NOT SPAWNING it — no seat is held, no limit is consumed, and nothing in flight is interrupted.
Two shapes, and both are obliged to name their own end:
set --until <instant> hold this scope until one absolute instant
set --from HH:MM --to HH:MM --tz quiet hours, every day, in a named zone
A pause ENDS BY THE PASSAGE OF TIME. There is no flag to clear and no job that has to run, which is why no failure can turn a pause into a permanent silence: kill the daemon, lose the machine, corrupt the config — when a daemon next ticks after the stated instant, it spawns, because that instant is in the past.
"lift" exists only so an owner who asked for 2.5 hours and finished in 30 minutes need not wait out their own decision. Nothing depends on it.
Subcommands
| Subcommand | What it does |
|---|---|
saphan serverd pause lift | Release a hold EARLY — a convenience, never the mechanism |
saphan serverd pause list | Show every pause and its state — held | standing | expired | lifted | UNREADABLE |
saphan serverd pause set | Set a hold on a scope — defer-until (one instant) or quiet-hours (every day) |
saphan serverd pause lift
Release a hold EARLY — a convenience, never the mechanism
lift stamps one live pause as released, with an actor and a ground on the record. The row is not deleted: a hold that happened stays readable as a thing that happened.
IT IS A CONVENIENCE. A pause already ends at its stated instant, on its own, with nothing running. This verb exists so an owner who asked for 2.5 hours and finished in 30 minutes need not wait out their own decision — no mechanism depends on it, which is precisely why its failure cannot silence a stream.
The engine is refused as an actor, by the same test that refuses it as the actor of a gate decision: ending a hold the owner set is a human decision, and the engine already has the only role it needs here, which is to let time pass.
Options
| Option | Type | Default | Description |
|---|---|---|---|
--actor | string | unset | the HUMAN releasing it — never the engine; required |
--ground | string | unset | why the hold is over early; required |
--id | int64 | 0 | pause row id to release; required |
saphan serverd pause list
Show every pause and its state — held | standing | expired | lifted | UNREADABLE
list renders every pause row, oldest first. It answers the question a silent stream raises: is it PAUSED or is it DEAD?
held holding right now. Carries resumes= and remaining= — the END, on the record, which is the whole distinction from a dead stream standing quiet hours in force but outside the window at this instant expired a hold that ENDED BY ITSELF. Left on the record deliberately, so a past silence stays explicable months later lifted released early by a named human, with a ground UNREADABLE a row this build cannot compute an end for. It holds NOTHING and the streams it names are RUNNING — shown loudly, because an owner who believes such a row is holding their stream is wrong in the dangerous direction
NO ROWS AT ALL means no stream here is paused. If a stream is quiet anyway, its silence has another cause — look at "serverd status" and the run record.
This verb reads the store, not the daemon. It answers while the daemon is down, which is exactly when the question gets asked.
Options
| Option | Type | Default | Description |
|---|---|---|---|
--stream | string | unset | show only pauses that can apply to this stream (fleet-wide rows are always shown) |
saphan serverd pause set
Set a hold on a scope — defer-until (one instant) or quiet-hours (every day)
set writes ONE pause row. If a live pause already exists for the same scope and kind it is MOVED, never stacked: the database refuses two live pauses for one scope, so a reader never faces two contradictory resume instants.
SCOPE is four optional filters, each meaning "any" when omitted:
--stream hold one stream --role hold one role across streams (master, executor, review, ...) --identity hold one seat — the shape for a seat under vendor pressure --machine hold one machine
With none of them given the hold is FLEET-WIDE. That is legal and it is loud: every surface renders it as "scope=FLEET".
WHAT IT REFUSES, and each refusal is the same principle — a pause must be able to name its own end:
--until in the past holds nothing; say what you meant --until more than 30 days out indistinguishable from an abandoned stream; close the stream instead, with a record --from equal to --to a window covering the whole day names no end --tz missing or unknown "nine in the evening" is not an instant until somebody says where --author or --ground empty a stream held by nobody, for no recorded reason, is a stream nobody will think to release
Options
| Option | Type | Default | Description |
|---|---|---|---|
--author | string | unset | who is holding this scope; required |
--from | string | unset | quiet-hours: window start, HH:MM |
--ground | string | unset | why; required |
--identity | string | unset | hold this seat (omit for any) |
--machine | string | unset | hold this machine (omit for any) |
--role | string | unset | hold this role (omit for any) |
--stream | string | unset | hold this stream (omit for any) |
--to | string | unset | quiet-hours: window end, HH:MM (exclusive) |
--tz | string | unset | quiet-hours: IANA zone the window is read in, e.g. Europe/Warsaw |
--until | string | unset | defer-until: the RFC3339 instant the stream resumes |
saphan serverd reload
Re-read the daemon's CONFIG (SIGHUP) — P2 has no monitor state to lose
saphan serverd start
Start the metering daemon (foreground by default; --detach to background it)
Start saphan serverd in the foreground: guard pidfile under the user-global RuntimeDir/serverd.pid (distinct from "saphan server"'s server.pid — both may run at once; §B0.3: exactly one serverd pidfile per user per machine), SIGHUP reloads CONFIG only, SIGTERM/SIGINT stop cleanly.
Before the pidfile is written, start reconciles (Plan §2.3): it scans run_record for rows still status=running whose process is confirmed alive and logs each one found. It never respawns them — reconcile-not-respawn, backstopped by the R8 dispatch lock even if this scan had a bug.
P2 carries zero monitor logic (that is P4). The one thing beyond lifecycle this loop answers is a manual trigger (SIGUSR1, see "serverd trigger"): one spawn through the shared spawn door, on operator demand only.
--detach (order serverd-detach-p5, §B3.1) re-execs this same binary detached from the invoking terminal (new session, log redirected) and returns once a bounded readiness handshake confirms the daemon is actually up — never a fork-and-lie. The guard (single-instance pidfile) always runs in the FOREGROUND child, never in the detaching parent.
Options
| Option | Type | Default | Description |
|---|---|---|---|
--detach | bool | false | re-exec detached (new session, log redirected) and return once the daemon is confirmed up, instead of blocking in the foreground |
saphan serverd status
Report whether the metering daemon is running
saphan serverd stop
Stop the running daemon (SIGTERM)
saphan serverd trigger
Signal the running daemon to perform ONE spawn through the shared door (manual smoke trigger)
trigger does not spawn anything itself: it hands a SpawnSpec to the RUNNING "serverd start" process (found via its pidfile) and signals it (SIGUSR1) to execute exactly one spawn through the shared spawn door (internal/cli.Spawn — the same runner.Spawn "saphan run" uses). The daemon stamps invoking_surface=demon — P1's third value, unused until this trigger — and the spawn is bound by the R8 dispatch lock exactly like any other caller: a stream already carrying a live run refuses R8, recorded, never a silent queue.
This is the metering path itself for masters (order serverd-w1 P3, Plan §4): a master session gains no spawn capability of its own — serverd is the ONLY caller that may request --role master, and it does so through this same shared door, so a master run_record row lands in the identical shape an executor's does (same Spawn call, same two-phase InsertSpawn/Finalize write, same cost_ledger aggregation). --role carries fleet_session's existing closed vocabulary (master | executor | review | design | scribe | advisor) — READ from spec.Validate's own source, never retyped here — and no new dictionary.
Options
| Option | Type | Default | Description |
|---|---|---|---|
--actor | string | unset | mandate holder's id (required) |
--arg | stringArray | [] | proc argv entry (repeatable, ordered) |
--backend | string | unset | backend: proc | claude-code | codex | openai-compat | agent-loop | qwen-code (required) |
--binary | string | unset | absolute path (resolved on this host) or $HOME/… (resolved on the execution host for transport=ssh, on this host otherwise) — PATH resolution is forbidden (required) |
--binary-sha256 | string | unset | pin the binary digest (R4 on mismatch) |
--brief | string | unset | kickoff brief path for LLM backends |
--budget-usd | float64 | 0 | claude-code only: --max-budget-usd limit |
--dry-run-arg | stringArray | [] | proc: declared dry-run argv entry (repeatable) |
--effort | string | unset | mandate.effort |
--effort-strict | bool | false | refuse when the backend cannot enforce effort (R6) |
--env | stringArray | [] | environment variable NAME to ADD to the backend's baseline env (repeatable) |
--ephemeral | bool | false | no-persist vendor mode; ILLEGAL for plan profile (R2) |
--laws | string | unset | ABSOLUTE path to the standing-law index the head must receive — REQUIRED for LLM backends; inlined into the prompt at every spawn and recorded by SHA-256 (R12 refuses an unreadable file) |
--model | string | unset | mandate.model — mandatory for LLM backends |
--outside-write-dir | stringArray | [] | request write access outside the workdir (refuses, R7) |
--profile | string | unset | capability profile: plan | execute (required) |
--provision | stringArray | [] | provision file <source>=<workdir-relative-dest> (repeatable) |
--resume | string | unset | resume the RECORDED vendor session key (R3) |
--role | string | unset | spawned session's fleet role: master | executor | review | design | scribe | advisor — spec.Validate's closed vocabulary, READ from it (required) |
--stream | string | unset | stream slug the run belongs to (required) |
--version-arg | string | unset | proc only: flag that prints the version string |
--wait | duration | 5s | poll this long for the daemon's trigger result before returning (0 = fire-and-forget) |
--wall-clock | duration | 0s | MANDATORY wall-clock limit (required) |
--workdir | string | unset | absolute workdir (required) |