WBS 3.4 is done both halves; issue 127 holds 4.2, 4.3 and catch-up

The controller's inbound is through a seam with both transports behind it, and
the store window is now the server's rather than the controller's memory. Seven
claims about that were asked of a running server rather than reasoned.

Wiring the controller's own subscription is what found issue 127: every event
name in the catalogue is still written the way a routing key is, so design 29's
derivation turns a consumer's declaration into a subject no emitter publishes.
Thirty-seven manifests, one that cannot be composed at all. It fails on the first
mesh raised on the new bus and not before, which is why nothing had caught it —
the conformance fixtures pin one emitter against one subject, and both halves of
that pair are correct.

The node-facing flows are unaffected: those subjects are the mesh's own and
derive from nothing a module declares.
This commit is contained in:
2026-09-27 00:55:07 +02:00
parent 4f93d304d7
commit f555d523c7
2 changed files with 153 additions and 29 deletions
+60 -29
View File
@@ -5,7 +5,7 @@ code:
- mesh-catalog modules/nats
- mesh-controller internal/catalogue
- mesh-lab scenarios
updated: 2026-09-26
updated: 2026-09-27
decisions:
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
- 02-DECISIONS/0106-the-bus-is-nats.md
@@ -242,36 +242,48 @@ pays for itself furthest away.
`x-event-id`. Claims checked against a running server are marked *verified* in the text, so
a reader can tell what was measured from what was reasoned. One limitation lifts with the
transport: a module may now call another's tool, which issue 049 recorded it could not.
- [~] 3.4 the controller's link on NATS — **the seam exists and the outbound half is through
it.** `Bus` is stated in the mesh's words (publish an event, declare to a node) rather than
a transport's, with an AMQP and a NATS implementation, both shipping: steps 1 to 4 leave
every node on AMQP, and both shipping is what lets one conformance fixture hold them to the
same envelope. The NATS one is checked against a real server, reading back from the stream
rather than from the code that wrote it.
- [x] 3.4 the controller's link on NATS — **both halves are through the seam, and the store
window is the server's.** `Bus` states the outbound in the mesh's words (publish an event,
declare to a node) and `Control` states the inbound (took it, dropped it, held it for the
store); each has an AMQP and a NATS implementation, and both ship, because steps 1 to 4
leave every node on AMQP and both shipping is what holds them to one envelope.
The seam turned out to be eight call sites — the same smallness that said this bus could be
replaced at all.
The outbound seam turned out to be eight call sites; the inbound was the larger half, and
the reason: every handler took the transport's own delivery type, so the loop could not move
without moving enrolment, reports, builds, upgrades and catch-up with it in one breath.
**The consume side's hard part is decided and tested**: the store window is a `nak` with a
delay rather than a delivery held in memory, which also means a controller restarting
mid-window loses nothing. Moving the holding into the server costs one thing — an older
report is redelivered after a newer was applied — and a report already carries the digest
of the declaration it is about, so supersession becomes a check rather than something the
controller remembers. Pure and tested without a bus, a store or a clock.
**The window (ADR 0083) is now what decides, once, for both.** On the bus the mesh has,
holding a message means an unacknowledged delivery kept in the controller, bounded by the
prefetch and lost if it stops. On the bus being built it is a `nak` with a delay: the
message stays the server's and the controller keeps only the moment it first could not take
it, so one that restarts mid-window has nothing to lose. Seven claims about that were asked
of a running server rather than reasoned — a report heard and gone from the work queue, one
held through a store outage and recorded when it returned, one let go once the bound passed,
a superseded one settled without being acted on, a heartbeat heard and nothing persisted,
both followed events acknowledged on a stream the controller had no ack subject for, and the
enrolment answer arriving at the address the request carried in its payload.
**Asking a tool is through the seam and loses two problems**: there is no reply queue to
declare and no correlation to check, because each account has one inbox prefix and an
answer cannot reach the wrong asker — which settles a cost the build code records having
paid, where every asker saw every result. And a tool nobody serves says so at once instead
of after the whole wait, which is the difference between "that module is down" and "that
tool is slow".
**Three things the wiring forced into the open.**
**A build is a different shape, not the same one.** It takes minutes, so it is work
submitted to a queue with the outcome returning to a reply subject the request carries —
the pattern design 25 §2 already sets for anything crossing a stream. It touches the
builder as well, so it travels with that conversion in step 4.
*Supersession is asked before the store, not after.* A report about a declaration the mesh
has moved past would otherwise wait out a restarting store to be written, and then overwrite
what the node is doing now.
Still outstanding: wiring the window decision into the loop, enrolment, and serving.
*Half of a report is not about a declaration, and that half is never stale.* What the machine
**is** — the tunnel it took over, the ports its own bundle holds, what an adopted node found,
a node moving its overlay key — reaches the mesh on a report and nowhere else. A rekey set
aside as stale is a node whose overlay key never moves, and no retry is coming, because the
node said it once. So staleness is asked only of a report that is purely an apply's account.
*The controller could not have consumed a module event at all.* Its account granted no event
subject to subscribe and no ack subject on the events stream, so every announcement would
have been redelivered for ever, refused by the permission list it already had. Both are now
granted, each subject named rather than by pattern — a controller subscribing every event in
the mesh is a permission list that has stopped saying what it is for. Its consumers are
**named beside the mesh's own streams rather than derived**, because the controller files no
manifest and authority cannot come from a declaration that does not exist.
Still outstanding: a build's own shape, which travels with the builder in step 4.
- [~] 3.5 the host's link on NATS — **the outbound half is through a seam**, mirroring the
controller's and still importing nothing of the mesh's own (ADR 0005): the host's own
interface over its own libraries, agreeing with the controller only because a fixture holds
@@ -349,6 +361,17 @@ module.
**Why here.** The links exist from step 3, so the flows that are not on the bus at all can move onto
it, and the beds that need a mesh living on NATS can finally run.
> **A blocker surfaced here that is not this step's to fix.** Every event name in the catalogue is
> still written the way a routing key on the bus the mesh has is written, so the derivation design 29
> §1 specifies turns a consumer's declaration into a subject **no emitter publishes** — thirty-seven
> manifests, and one that cannot be composed at all. Nothing fails on the bus the mesh runs on
> today, where a routing key is matched literally; it fails on the first mesh raised on the new bus
> and not before, which is why wiring the controller's own subscription is what found it. Opened as
> [issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md).
> It holds 4.2, 4.3 and the catch-up half of 4.5; the node-facing flows — enrolment, reports,
> heartbeats, a build's outcome — are unaffected, because those subjects are the mesh's own and
> derive from nothing a module declares.
- [ ] 4.1 **the full genesis bed** — a mesh raised on NATS from nothing and living on it: a node
enrols over TLS with a claimed token and the enrolment user cannot read a declaration; a push
is held while the store restarts and applies after, nothing lost or duplicated; a node that
@@ -358,8 +381,10 @@ it, and the beds that need a mesh living on NATS can finally run.
in the payload and not the transport field the consumer's ack has claimed. The server-enforced
permissions were proved at step 1 and are not re-proved here
- [ ] 4.2 a build source's change reaches the builder over the bus, and the build that follows is
the one the change asked for
- [ ] 4.3 an installation completes over the bus, with the same outcome as the path it replaces
the one the change asked for — **blocked by
[issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)**
- [ ] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
**blocked by the same**
- [~] 4.4 a person's client — **the account is done**: a person is not a module and holds no
seat, so their authority is a list of tools (or `*` for an administrator) and nothing else.
Held to four properties, each a way of being wrong that would not announce itself: nothing
@@ -370,7 +395,13 @@ it, and the beds that need a mesh living on NATS can finally run.
Still to build: the client program itself — the command line and the MCP surface over it.
It needs nothing from the consume side, so it is not blocked by step 3.
- [ ] 4.5 reports and catch-up: a node that was unreachable catches up rather than losing them
- [~] 4.5 reports and catch-up: a node that was unreachable catches up rather than losing them —
**the reports half is in and proved against a server** (3.4): held through the store's
absence by the server rather than by the controller, superseded ones settled by the digest
they carry. The catch-up half is where issue 127 bites hardest: the controller replays a
build announcement under its **own** name rather than the builder's, so a catalogue
filtering the builder's subject hears nothing. Whether the controller may sign an event as
another module is a design question, not a wiring one, and it is open in that issue.
**Done when.** Each converted flow is proved against the behaviour it replaced, and the full genesis
bed is green. **Observation is not in this step** — heartbeats, conditions and key-value state are