Saphan StudioDocs
Reference/Command reference

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

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

SubcommandWhat it does
saphan serverd mailboxDrop an owner directive into the mailbox — INSERT only, zero dispatch (order serverd-w1 P5)
saphan serverd pauseHold a stream's automatic spawns until a named instant — zero cost, and it ends by itself
saphan serverd reloadRe-read the daemon's CONFIG (SIGHUP) — P2 has no monitor state to lose
saphan serverd startStart the metering daemon (foreground by default; --detach to background it)
saphan serverd statusReport whether the metering daemon is running
saphan serverd stopStop the running daemon (SIGTERM)
saphan serverd triggerSignal 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)

saphan serverd mailbox --stream <s> --directive <text> [flags]

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

SubcommandWhat it does
saphan serverd mailbox listShow mailbox directives and their state — pending | delivered | retracted | exhausted
saphan serverd mailbox retractWithdraw a PENDING mailbox directive — its own state, never delivered=1 (order mailbox-spawns-fresh-and-retracts §3)

Options

OptionTypeDefaultDescription
--directivestringunsetdirective text, UNSIGNED in W1 (Plan §2.4); required
--streamstringunsetaddressee stream — first-class (PRAWO ADRESATA); required
--urgentboolfalsecross 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

saphan serverd mailbox list [--stream <s>] [flags]

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
OptionTypeDefaultDescription
--streamstringunsetshow 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)

saphan serverd mailbox retract --id <n> --actor <name> --ground <why> [flags]

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
OptionTypeDefaultDescription
--actorstringunsetthe HUMAN withdrawing it — never the engine; required
--groundstringunsetwhy the directive is no longer to be delivered; required
--idint640mailbox 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

saphan serverd pause

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

SubcommandWhat it does
saphan serverd pause liftRelease a hold EARLY — a convenience, never the mechanism
saphan serverd pause listShow every pause and its state — held | standing | expired | lifted | UNREADABLE
saphan serverd pause setSet 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

saphan serverd pause lift --id <n> --actor <name> --ground <why> [flags]

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
OptionTypeDefaultDescription
--actorstringunsetthe HUMAN releasing it — never the engine; required
--groundstringunsetwhy the hold is over early; required
--idint640pause row id to release; required

saphan serverd pause list

Show every pause and its state — held | standing | expired | lifted | UNREADABLE

saphan serverd pause list [--stream <s>] [flags]

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
OptionTypeDefaultDescription
--streamstringunsetshow 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)

saphan serverd pause set (--until <instant> | --from HH:MM --to HH:MM --tz <zone>) --author <name> --ground <why> [flags]

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
OptionTypeDefaultDescription
--authorstringunsetwho is holding this scope; required
--fromstringunsetquiet-hours: window start, HH:MM
--groundstringunsetwhy; required
--identitystringunsethold this seat (omit for any)
--machinestringunsethold this machine (omit for any)
--rolestringunsethold this role (omit for any)
--streamstringunsethold this stream (omit for any)
--tostringunsetquiet-hours: window end, HH:MM (exclusive)
--tzstringunsetquiet-hours: IANA zone the window is read in, e.g. Europe/Warsaw
--untilstringunsetdefer-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 reload

saphan serverd start

Start the metering daemon (foreground by default; --detach to background it)

saphan serverd start [flags]

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

OptionTypeDefaultDescription
--detachboolfalsere-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 status

saphan serverd stop

Stop the running daemon (SIGTERM)

saphan serverd stop

saphan serverd trigger

Signal the running daemon to perform ONE spawn through the shared door (manual smoke trigger)

saphan serverd trigger --stream <s> --backend ... (same spawn flags as "saphan run") [flags]

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

OptionTypeDefaultDescription
--actorstringunsetmandate holder's id (required)
--argstringArray[]proc argv entry (repeatable, ordered)
--backendstringunsetbackend: proc | claude-code | codex | openai-compat | agent-loop | qwen-code (required)
--binarystringunsetabsolute 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-sha256stringunsetpin the binary digest (R4 on mismatch)
--briefstringunsetkickoff brief path for LLM backends
--budget-usdfloat640claude-code only: --max-budget-usd limit
--dry-run-argstringArray[]proc: declared dry-run argv entry (repeatable)
--effortstringunsetmandate.effort
--effort-strictboolfalserefuse when the backend cannot enforce effort (R6)
--envstringArray[]environment variable NAME to ADD to the backend's baseline env (repeatable)
--ephemeralboolfalseno-persist vendor mode; ILLEGAL for plan profile (R2)
--lawsstringunsetABSOLUTE 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)
--modelstringunsetmandate.model — mandatory for LLM backends
--outside-write-dirstringArray[]request write access outside the workdir (refuses, R7)
--profilestringunsetcapability profile: plan | execute (required)
--provisionstringArray[]provision file <source>=<workdir-relative-dest> (repeatable)
--resumestringunsetresume the RECORDED vendor session key (R3)
--rolestringunsetspawned session's fleet role: master | executor | review | design | scribe | advisor — spec.Validate's closed vocabulary, READ from it (required)
--streamstringunsetstream slug the run belongs to (required)
--version-argstringunsetproc only: flag that prints the version string
--waitduration5spoll this long for the daemon's trigger result before returning (0 = fire-and-forget)
--wall-clockduration0sMANDATORY wall-clock limit (required)
--workdirstringunsetabsolute workdir (required)