GET /v1/gates is served by the engine door's first read verb (gates), which calls the
owner-queue projection. Two consequences an operator should know before reading the body:
The read reads the record. It does not scan the disk, and it does not write. Point the
process at the workspace whose queue you want; it does not aggregate across workspaces.
This paragraph used to say the opposite — "the projection re-scans <workspace>/worktrees/*
on every invocation … so the answer is always fresh — and the 'read' does write registry
rows" — and that is the behaviour the owner ruled out on 2026-08-28. What you get instead
is stated in full below, because a read whose freshness rule you do not know is worse than a
slow one.
The body's field names are the record's, not new ones: pending, stream, gate,
state, action, command, whose_move, as_of_ts. as_of_ts is the time of the
read, never the time any gate was raised — nothing in the record carries an emission time,
so any "age" you see elsewhere is computed. Each row's command keeps <decision> and
<you> unsubstituted: the engine composes commands and only humans execute them.
⚠ Conformance of this body with the signed GET /v1/gates schema is UNVERIFIED on any host
without the provisioned contract — see Verifying conformance below. The shape is the
projection's, which is the only vocabulary available when the document cannot be read.
A second, differently-named refusal exists for this operation: if the process cannot reach its
record (no --workspace, or a store that will not open) it answers 503 with the Problem type
urn:saphan:gateway:record-unavailable and Retry-After: 30. That is a configuration
problem an operator fixes in one command, as against not-implemented, which waiting and
configuring never fix. Do not treat the two as one status.
A read is fresh to the last write the engine performed. Nothing more, and it is worth
being precise about, because the previous rule was "always".
A leg that has just written - state: stop-2-ready into its _EXEC_STATUS.md is not in
/v1/gates or on /console yet. The file is on disk; the record has not been told. Refreshing
the page does not ingest anything — it never re-reads a channel file, so no amount of reloading
will make that stream appear.
Nothing here is a workaround to reach for: an operator who wants the record refreshed should run
saphan bus reconcile (or saphan fleet show, or saphan fleet next --stream <slug>), not open
a page.
The practical answer to "I hand-edited a channel file; will the console show it?" is no —
run saphan bus reconcile against that workspace and it will. ⚠ RETIRED 2026-08-28 (r2 review,
F2): both of these lines used to say "run any saphan fleet verb", which sent the operator to
a coin flip — 5 of the 7 subverbs ingest nothing.
This is the owner's accepted trade, in the owner's own words (2026-08-28): "the write must go
through the binary for now. But not the read. Otherwise this will not work." The read path pays
no scan and takes no write lock, which is why it can be served by a process that holds a
read-only handle to the record at all.
The register is bounded. A request that names no limit receives the contract's default page
of 50 rows, never the whole fleet — the ceiling is 200, a limit outside [1,200] (or
one that is not a number at all) is treated as absent and falls back to 50, and this operation
answers no 400, because contract 3.0.1 declares only 200, 401 and 503 on it.
Two fields, and reading only one of them is how an operator concludes the fleet is smaller
than it is:
total_count is the fleet's size, computed before any page bound. streams.length is
this page's size. On any fleet larger than 50 they differ, and only the first one answers
"how many streams do I have".
nextCursor is null ONLY at the end of the list. A non-null value is an opaque token:
store it, hand it straight back as ?cursor=<token>, never parse it and never compose one.
It is not /sync's since — that cursor is of a delta, this one is of a page.
Paging is not stable, and the walk can silently come up short. The register is ordered by
updated_tsdescending and the cursor is that order's coordinate. ⚠ AMENDED IN PLACE
2026-08-28. This sentence used to continue "Every read reconciles, so the record genuinely
moves while you page it". That clause is now false: GET /v1/streams replays the record and
writes nothing, so your own reads no longer move anything under you. The record still moves —
an engine write moves it (a gate, a merge, a saphan return, a saphan bus reconcile),
and such a write can land between two of your pages. The window is narrower; it is not closed,
and the shortfall below is unchanged whenever a write does land: a stream touched between two of your requests
takes a newer timestamp and jumps to the front of the order — ahead of a cursor that is
still perfectly valid — and is then never served to you. Nothing about the reply says so.
nextCursor: nullstill means end of list, and the response is still correct: it is
the right page of the order as it now stands. Measured on a six-stream register at ?limit=3,
with one unseen stream touched between the two requests: 5 of 6 streams delivered, and
3 of 3 assertions — nextCursor: null, total_count: 6, no repeated row — hold exactly as
the contract promises. The row that moved is simply absent.
Your only client-side detector is to compare the rows you collected against total_count,
and a divergence is not necessarily a defect: a fleet that gained or lost a stream while
you walked it diverges the same way. Treat a shortfall as "re-read from the top", never as an
error to report. If you need a consistent snapshot, take the whole register in one request
(?limit=200, up to the ceiling) rather than walking it.
Paging the whole register by hand, from the loopback listener:
# 1. first page — 50 rows, and the token for the next onecurl -H "$SAPHAN_AUTH" --fail --show-error -s http://127.0.0.1:7656/v1/streams \ | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["streams"]), "of", d["total_count"], "next:", d["nextCursor"])'# 2. next page — repeat with the token, until nextCursor is nullcurl -H "$SAPHAN_AUTH" --fail --show-error -sG http://127.0.0.1:7656/v1/streams \ --data-urlencode "cursor=<the nextCursor from step 1>"
⚠ Not paging is a silent wrong answer, not an error. A caller that reads the first page and
stops gets a well-formed 200 describing 50 of your streams; nothing in that response is
malformed, and nothing warns you. total_count is the only field that contradicts it.
GET /v1/ledger returns one window of cost_ledger, rolled up into buckets, one page at a
time. Four query parameters, all optional, all with contract defaults: window
(mine·today·7d·30d·all, default mine), groupBy
(day·envelope·stream, default day), limit (1–200, default 50) and cursor.
# today, by daycurl -H "$SAPHAN_AUTH" --fail --show-error -s 'http://127.0.0.1:7656/v1/ledger?window=today'# the last 30 days, by stream, 10 buckets at a timecurl -H "$SAPHAN_AUTH" --fail --show-error -s 'http://127.0.0.1:7656/v1/ledger?window=30d&groupBy=stream&limit=10'
1. total and every amount are the REAL meter — cost_real_usd — and nothing else is
added into them.cost_ledger carries two meters. The real one is money somebody is billed
for; the virtual one is what a subscription seat would have cost if it were metered, and it
is not money. The reply names which one you are reading in its meter field.
⚠ So a fleet running entirely on subscription seats answers with every amount at 0.00,
and that is correct. The engine's own writer forces cost_real_usd to zero for
subscription-covered, local-energy and deterministic rows. Read window_rows beside the
total: total: 0.00 with window_rows: 0 means nothing happened; total: 0.00 with
window_rows: 412 means 412 acts, all covered by an allowance. Those are different
sentences and the total renders them identically. The virtual meter is not published by this
operation at all — contract 3.0.1's Ledger schema has no field for a second amount. That
is a contract gap, not a missing feature.
2. groupBy=envelope is answered with day, and the reply tells you so. The contract
declares three groupings; the record supports two. There is no spend envelope anywhere in the
record — no cap column, no quote column, nothing the contract's EnvelopeAlarm (envelope,
pct, cap, spent, derivedFrom) could be built from. So the operation serves the default
grouping and reports "groupBy": "day" in the reply. Read that field rather than assuming
you got what you asked for:
curl -H "$SAPHAN_AUTH" --fail --show-error -s 'http://127.0.0.1:7656/v1/ledger?groupBy=envelope' \ | python3 -c 'import json,sys; print("served groupBy:", json.load(sys.stdin)["groupBy"])'# served groupBy: day
This operation answers no 400 — contract 3.0.1 declares only 200, 401 and 503 on
it — so every unrecognised window, groupBy, limit or cursor falls back to the
contract's own default. That is why the reply echoes window and groupBy: they are the only
place a mistyped parameter becomes visible.
3. The cursor is OPAQUE and this process is the only one that can mint one. Store it,
hand it back, never build one. A token this process did not issue — hand-composed, edited, or
minted by a previous process before a restart — does not verify, and the read answers the
first page rather than an error. So a walk interrupted by a gateway restart costs one
repeated page and then continues; it does not fail and it does not loop.
# 1. first pagecurl -H "$SAPHAN_AUTH" --fail --show-error -s 'http://127.0.0.1:7656/v1/ledger?limit=10' \ | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["rows"]), "of", d["total_groups"], "next:", d["nextCursor"])'# 2. repeat with the token until nextCursor is nullcurl -H "$SAPHAN_AUTH" --fail --show-error -sG http://127.0.0.1:7656/v1/ledger \ --data-urlencode "cursor=<the nextCursor from step 1>" --data-urlencode "limit=10"
A walk pins its window. The bounds are resolved once, on the first page, and carried
inside the cursor — so from and to are identical on every page of one walk even though
window=today names a moving interval. Rows written while you are paging appear in the next
walk, not this one. Buckets are ordered by key ascending (chronological for day), and the
coordinate is that key, so a bucket whose amount grows mid-walk does not move under you.
and the body of the 503 carries urn:saphan:gateway:record-unavailable — the configuration
discriminator — while naming a trust root pin and a path under a home directory.
Cause.GET /v1/streams builds a fleet view, and a fleet view reads the signed machine
registry. That registry verifies every identity row against the owner's trust root pin,
loaded from ~/.saphan/trust/root.key.pub — resolved against the $HOMEof the gateway
process, not of the person typing curl, and not of the workspace. GET /v1/gates builds the
owed-gate queue, which never reads the machine registry, so it is untouched.
Two consequences that explain the shape of the symptom:
The pin is only loaded once the registry has at least one machine row. Zero admitted
machines short-circuits to a lawful empty answer, so a fresh workspace never sees this —
it appears the day the first machine is admitted, which is rarely the day anyone changed the
gateway.
Because $HOME is the process's, this bites hardest when the gateway runs as a service
account: systemd units with no User=/Environment=HOME=, containers, and sudo without
-H all hand the process a $HOME that is not the owner's.
Fix, in order:
Find the $HOME the gateway process actually has — not your shell's:
Check the pin there. doctor is the health check for exactly this material:
sudo -u <the gateway's user> -H saphan trust doctor
If the owner already has a root key (the normal case — it was minted once, on the
owner's machine), copy only the public half into that home. Never the private key, and
never mint a second root: a fresh root would not verify a single existing identity row.
If the fleet has no root key at all yet, that is a different act performed once by the
owner on the owner's machine — saphan trust init — and not something to do here.
For a systemd unit, pin the home explicitly rather than relying on the default, so the
next restart cannot undo step 3:
Note — the class on the wire is wrong, deliberately, for now. This is a provisioning
defect: a file this host was never given. The gateway reports it as
urn:saphan:gateway:record-unavailable, which says "the record is unavailable" and sends
Retry-After: 30 about a condition that waiting never fixes. Reclassifying it needs a
discriminator the door does not yet carry, and inventing one here would let a genuinely
missing trust root be rendered as "this workspace has admitted no machines" — the exact
substitution the fail-closed loader exists to prevent. So the misclassification is carried
on purpose and deferred to its own wagon, and this runbook is what stands in for it until
then. The refusal text is the part you can trust: it names the missing path and says the
machine registry is what asked for it.
This operation answers what no other operation on this surface could: who your seats are, how
much of each one's session and week is spent, and when each window renews. It is also the most
misreadable reply the gateway serves, because every one of its honest absences looks like a blank.
Four things, and the last two are the ones that cost real money if you skim them.
1. It never probes. Every number is as old as its own age_seconds says.
saphan fleet usage reads seats by spawning the vendor binary once per seat. This route does
nothing of the kind — it reads back what those probes already recorded, so it costs one
database query no matter how many seats you have and it can be polled by a dashboard without
spending anybody's quota. The price of that is age: a seat's reading.as_of_ts is when the
reading was measured, and reading.age_seconds is how long ago that was. A percentage with no
age reads as current no matter how old it is, which is why both ride on every row.
To refresh them, run the probe — at a terminal, deliberately, because it costs vendor calls:
saphan fleet usage --workspace /srv/saphan/engine
1a. But an old reading is not served as if it were current — it is SETTLED first, and
reading.source tells you whether anything overruled it. Age alone is a number a client can
skim past; this is the same judgement made for you, on the reply's own clock, before the state is
written. Two rules fire, and neither of their outcomes is free:
reading.source
what happened
what else the row carries
stored
nothing overruled the row — this is the ONLY shape in which a free state means the seat can still be spent
the stored state and percentages, unchanged
stale
the reading aged past the 60-minute staleness ceiling (or past its own named reset instant) with no re-probe since, so it is now state: "unknown"
stored_state, settled_reason
run-fact
a lane-exhausted run started on this seat strictly after the reading was taken, so it is now state: "exhausted"
remark: measured on this fleet — a seat once read 0% session and 100% week at the same instant; the binding line is whichever window is SPENT, not the state word. A stale free rendered as spendable is the single failure that costs a whole leg's wall-clock and returns lane-exhausted.
remark: stored_state appears ONLY on a row settlement changed, so its presence is itself the signal; a row without it was not overruled.
The settlement is additive, never destructive: stored_state is what the table still holds, so
you can see both that the row says free and that this engine refuses to act on it. But the
percentages go with the verdict — a settled row publishes used_pct: null for both windows,
because an old number cannot survive its own state, and a state: "unknown" row still showing
used_pct: 24 reads to every client as a seat with 76% of its session left.
2. A seat is in one of THREE states, and collapsing any two of them gives you a wrong screen.
Branch on knowledge first:
knowledge
what it means
what else the row carries
measured
a reading exists
reading, with its own age
never-measured
the seat can be read and nobody has ever probed it
never_measured, naming the probe to run
unmeasurable
no reading can exist for this seat as it stands
unmeasurable, naming which of six refusals applies
remark: never-measured is not a broken seat and unmeasurable is not a broken vendor — the first says the instrument was never pointed here, the second says our instrument does not reach this seat.
remark: measured 2026-08-31 on this fleet — 21 seats probed, of which 5 answered unmeasurable for two different reasons, so a screen that renders one sentence for all of them is already wrong here.
remark: an ssh machine whose worktrees_root is set but relative is unmeasurable, not never-measured — the root must be ABSOLUTE for a far workdir to be derivable, and a relative one resolves against whatever directory the caller happens to be in. Following a never-measured instruction to probe such a seat gets you a refusal, not a reading.
An unmeasurable row is an honest statement about the instrument, never a slight on a vendor.
A fleet routinely runs seats of several backends side by side on purpose, and one whose usage
reader has not been written reads
usage probe unsupported for backend "qwen-code" — no vendor-native usage reader is registered
because nobody has written that reader; the vendor withholds nothing. A seat that reads
identity "…"'s seat carries no bound profile home (seated before cast-surface) — re-seat to bind a profile was created before profile homes were bound, and the sentence names the repair.
3. used_pct is ALWAYS SPENT, NEVER REMAINING — and your two backends report opposite
polarities. Claude Code prints usage spent (session=24%); Codex prints headroom
remaining (headroom=81.0%). The record stores only "used" semantics and records what it
translated from, so a Codex seat measured at 81% headroom appears here as used_pct: 19 with
reported: "free". A client that renders used_pct as headroom has inverted a seat's
capacity, which is how a dispatch lands on the fullest lane in the fleet. reported is the
provenance: used was stored as read, free was translated, unknown is a row written before
that column existed.
used_pct: null and used_pct: 0 are opposite verdicts: 0 is the freest possible seat and
null is a window nobody may plan against. They are never the same bytes here.
4. Four facts are NOT in this reply, they are NAMED in fields_unserved, and the renewal
instant is the one that used to bite you. The durable table (seat_usage_reading) has columns
for the state, the two percentages, the per-model map, each window's own reset clause since
migration 59, and the one first-printed clause it always had. It has no column for the vendor's
own text. Everything below follows from that:
field
why it is absent
what to do
a window's resets_at on a reading written before migration 59
that older row carries one clause — whichever the vendor printed first, the session's — and binding it to a window needs the vendor's verbatim text, which is not stored. A reading written SINCE answers both windows from their own columns
re-probe: the table is append-only and this reply reads the newest row per seat, so one probe replaces the answer. No migration is owed
reading.unknown_reason, on a STORED unknown only
there is no reason column, so an unknown the table already held — a headroom-stale rollout, an unparseable probe — is the same seven bytes either way. An unknown this reply produced itself by settling (§1a) does carry its sentence, in settled_reason
re-probe; the terminal prints the reason. Check reading.source first: only stored is the unexplainable kind
the Codex rate window (primary/secondary)
folded into the session/week columns at write time; the name is not stored
re-probe
the vendor's own text
never stored
re-probe
headroom-stale is NOT headroom-exhausted, and this reply can only tell you one of them.
Exhaustion has its own state and is distinguishable here (state: "exhausted"). Staleness — a
Codex rollout reading older than its ceiling, on a seat that is otherwise perfectly dispatchable —
arrives as state: "unknown", and whether it can say why depends on where the staleness was
decided: one this reply settled itself carries source: "stale" and a settled_reason naming the
age and the ceiling; one the table already stored that way carries source: "stored" and no
reason, because there is nowhere in the row to put one. Treating those seats as dead idles working
capacity; treating them as live dispatches onto a lane you have not measured. When a seat reads
unknown, re-probe it before you decide anything about it.
⛔ Nothing here ever synthesises a date. When a window has no resets_at, it carries
reset_unknown_reason, and there are three different ones, which is the whole reason the
field exists:
the vendor printed none because nothing is spent in this window — a 0%-used session genuinely
has nothing to renew. Four of this fleet's eight readable claude seats were in this state on
2026-08-31.
the durable reading carries one clause and does not store the vendor's text — the instant was
never written down. Re-probe.
this reading carries no number for this window at all — there is nothing for an instant to be
about.
And one thing that is deliberately NOT here: nine seats.seats is taken from the signed
machine registry, machines in service only — never from the identity table. On this fleet the
identity table holds 29 subjects while machine list resolves 20 live seats; the extra nine are
seats of retired machines and identities never placed on one. They would render as real seats
whose probe has not run yet, and no run can ever be dispatched to any of them.
seats_on_retired_machines and machines_retired are published so that "we dropped the phantoms"
is auditable from the reply instead of asserted here.
This is the first operation in this gateway whose signed contract asks for more than the record
holds, and it is the first that tells you so in the reply rather than in a release note. Nothing
below is a bug report: it is what the operation IS on this release, and every sentence of it is
readable from the answer itself.
1. Four filters do not narrow anything, and the reply names them.capture_index — the one
table that holds a spark — has eleven columns: id · ts · kind · actor · stream · ref · sha256 · sig · blob_ref · ack · ack_ts. There is no state column, no tags column and no text column. So:
you send
what happens
why
window=…
applied
capture_index.ts exists. mine is served as the whole record — see below.
state=captured
applied, derived
it matches every row, because no row can be anything else.
state=worked
NOT applied
nothing in this release can write worked.
q=…
NOT applied
q searches CONTENT, and content is not in this index.
tag=…
NOT applied
there is no tags column.
since=…
NOT applied
since is a DELTA cursor for a client cache, not a page cursor. Use cursor.
limit · cursor
applied
the page is bounded at 50 by default and 200 at most.
remark: this table is the disposition of a FILTER, never a statement about your data — a row missing from a q= search was not excluded, it was never searched for.
remark: measured 2026-08-29 against store/migrate.go migration 13; the day capture_index grows a state or tags column, the two rows that say NOT applied are the ones that go stale first.
2. An unapplied filter returns MORE than you asked for, never less — and that is a decision.
Ask for ?state=worked and you get the sparks you would have got with no filter at all, plus
filters.state.served: false. The other honest answer was an empty list, and it was rejected:
to a client that reads only sparks, an empty list says you have no worked sparks, which is a
confident false negative. A wider page says here are your sparks, which is true and merely
wider than you asked for. So: check filters_complete (a boolean) or filters_unserved (an
array of parameter names) before you render anything as a search result.
3. text is empty on every row, and it is the record's doing, not the door's. The Spark
schema requires text, so the key is always present — and its value is always "". A capture
stores its sha256 in the index and its bytes in the evidence store (CouchDB), under
blob_ref, and this gateway process opens no evidence store: the engine door reads no config
by design. Each row carries text_available: false and a text_absence_reason naming which of
the two cases it is — never durably stored (ack: false) or stored where this door cannot
read it (ack: true). The reply's text_rows is how many rows on this page did carry text; it
is 0 on every page in this release, and it is counted rather than asserted.
⛔ sha256 is not a checksum of text. It is custody over the ORIGINAL bytes, while the
stored form is redacted. The two legitimately differ, and a client that presents one as a proof
of the other will eventually accuse the record of corruption.
4. id is the record's id, not the ULID the write side describes.POST /sparks says the
client mints a sortable ULID — and POST /sparks is one of the operations this gateway refuses.
The ids you read here are capture_index row ids in decimal. They are stable, and they are what
GET /sparks/{sparkId} will have to resolve when that operation lands.
5. window=mine is served as all, exactly as /v1/ledger serves it.mine asks whose
traffic, which needs an authenticated subject; this process has none, so the honest render is
the local operator's own view of their own workspace. The reply's window field says which
token was actually served, and from/to are the bounds it resolved to — read those, never the
token you sent.
6. Paging is stable, and unlike /v1/streams it cannot skip a row. The cursor's coordinate
is the row id, which never changes, and the table is append-only; a spark captured mid-walk
takes the highest id and is simply on your next walk rather than lost from this one. The token
is opaque and MAC'd: do not parse it and do not compose one. A token this process did not
mint — including one from before a restart, and one presented with different filters — does not
verify and you are served the FIRST page of the window you asked for. That costs one repeated
page after a restart; it is not a loop.
Expect 200, a shelves array, a shelvesUnserved array, a shelvesComplete boolean and a
nextCursor (a string, or null at the end). Read shelvesComplete beside any count you
show a human — that is the contract's own instruction and § 2 below is why.
1. A policy here is a named set of rules that binds machines and seats. The machine sets a
ceiling and a seat may narrow it and may never widen it. So a shelf's count is how many
rules of that family stand in the record, and the record it is counted over is the signed
machine registry — identity_record rows verified against the owner root pin, not the fleet
register and not any table the read's own handle holds.
shelf
binds
one rule is
confinement
machines
one machine in service whose row carries a believed filesystem-confinement measurement.
seats
seats
one seat seated on a machine in service — identity, slot, role, backend, lane, billing class.
sessions
machines · seats
one (machine, slot) that carries an explicit concurrent-session ceiling.
toolchain
machines
one (machine, backend) agent-binary registration.
egress
machines · seats
NOT COUNTED — see § 2.
remark: measured 2026-08-29 against registry.Machine / registry.MachineIdentity; a shelf's count is a count of RULES IN THE RECORD, never of hosts, seats or capacity.
remark: a retired machine is counted nowhere, and its seats with it — its rules bind nothing; machines_retired is published so that absence is auditable rather than assumed.
remark: sessions counts only ceilings somebody wrote. A slot with no configured value reads as the build's default of one live run, and a default nobody wrote is not a rule.
remark: confinement counts the believed measurement (Confinement.Effective). A row this build cannot reason about projects as unprobed; when any exist, the shelf's note says how many and tells you to re-probe.
2. Egress is in shelvesUnserved, and an empty shelf would have been the wrong answer.
An egress policy in this engine is an authored document, not a registry row: it is compiled
from a document, and a run that asks for one (saphan run --egress-policy) is judged by the
workspace's single canonical document. ⚠ The design's machine-and-seat intersection — the seat
narrowing its machine and never widening it — ships and is called by nothing, so no machine
and no seat is assigned a policy today; this paragraph described that intersection as how a run
IS judged And the shipped default remains that
nothing is assigned at all: a run that does not ask is judged by no policy. None of that is
in the machine registry, and this door opens no document and reads no config. So the shelf is published by name, with its reason, and
shelvesComplete is false. ⛔ The machine registry's own egress field is not this shelf:
it is a measured capability — which rung of the cut a host CAN perform — and counting it would
answer how many hosts can cut egress under the name how many egress rules exist. A host that
cuts egress perfectly and has been handed no allow-list has a full capability and zero rules.
3. shelvesComplete is computed, never declared, and it is true if and only if
shelvesUnserved is empty. It is a fact about the whole catalogue, not about the page you
are holding — so is shelvesUnserved — because a client walking to page 2 must not be told every
shelf is served just because the unserved one was on page 1.
4. Two different zeros, and registry_present is how you tell them apart. A count of 0 can
mean no machine has ever been admitted to this workspace or machines are admitted and none
carries a rule of that family. registry_present: false says it is the first, and
registry_note says it in words. An absent registry does not make a shelf unserved:
unserved means this installation cannot answer, while an absent registry is an answer.
5. rules_total is not how many rules the fleet has. It is the sum of the served shelves;
every unserved shelf contributes an unknown number. machines_in_service, machines_retired and
seats_in_service are the denominators every count was taken over, published so the arithmetic
can be checked without reading the source.
6. This reply is COUNTS ONLY. No machine id, no host, no ssh user or port, no seat name, no
config dir, no binary path, no registry path. There is no row grant that could narrow it — the
machine registry has no stream column for one to select on. If a future release puts an identifier
on this operation, treat that as widening a surface, not as an enrichment.