Files
hq/02-DECISIONS/0134-the-mesh-says-what-it-applied.md
jschoubben 891c7a945e ADR 0133 and 0134: who runs migrations, and the mesh saying what it applied
0133 — a module owns its migrations and the mesh owns when they run. A container declares what must
run before it; the mesh derives the gated step from the resource it precedes, so the image, the
environment and the credentials come from the one place they are described. The module owns the SQL,
the dialect and the lock; the mesh owns the moment and refuses to start a version whose step failed.
Per node, with no level: a step that ran once somewhere leaves every other machine ungated, and
'once, mesh-wide' is what holding a seat already means.

0134 — the pipeline is observable from a merge to an artifact and goes dark at the machine. What a
node now runs, and what it refused, become facts under the control plane's own seat, emitted when
what a machine runs changes rather than on every convergence pass.

Design 32's lifecycle carries both; issue 133 points at them as what ends the matter it opened.
2026-09-28 11:45:47 +02:00

8.2 KiB

topic, status, date, deciders, reconstructed
topic status date deciders reconstructed
the mesh accepted 2026-09-28 jochen false

134. The mesh says what it applied

Context

The pipeline is observable on the bus from a merge to an artifact, and modules already plug into it: the forge emits pull.merged, the build machine's seat emits built, the catalogue emits registered, upgraded and rebuild-needed, providers emit postgres.database.provisioned and its siblings. Things consume them today — the catalogue consumes built, each provider consumes its own provisioning events, model-usage consumes *.usage.*, the audit logger consumes **. Nothing had to be invented for any of that; subscribing is plugging in.

It goes dark at the moment it touches a machine. A host applies a declaration and reports to the control plane on the control branch, which only the control plane may read — correctly, because a report carries what a machine is and enrolment travels the same way. So nothing on the mesh says this machine now runs version Y of module Z, or that it refused to, or why.

What that cost on 2026-09-28, in one morning:

  • A build result the store refused was visible only to whoever was waiting on that build's reply. For three quarters of an hour the mesh built things and recorded none of them, while the overview said every module was current (issue 133).
  • A module crash-looping at start — 338 restarts — was found by reading a container's logs by hand. Nothing on the bus said the mesh's graph had stopped learning.
  • A run-once step that fails now stops an upgrade by design (ADR 0133), and the same silence would cover it: the version simply would not appear.

And the one thing the control plane does emit is refused by its own account. Answering a catalogue that asks to catch up, it publishes each recorded build under mesh.mod.control-plane.event.… — a module namespace for a module that does not exist. Its own permissions refuse it, so a catalogue that restarts gets nothing and keeps its gap. The control plane has facts to state and nowhere to state them.

Decision

The mesh emits the deploy half of the pipeline as facts on the bus. What a machine now runs, and what it refused to run and why. Both are facts about the mesh doing its work, in the same form as every other fact on the bus, so anything that wants them subscribes the way the catalogue subscribes to built.

The control plane states them, as the holder of the mesh-controller seat. Its facts live under the seat's own namespace, which is where a role's events belong (ADR 0121, ADR 0129) and which survives the control plane being replaced. That is also what gives the catch-up replay a subject it may publish instead of an invented module namespace.

Emitted when what a machine runs changes, not on every convergence pass. A host reconciles continuously and reports each time; a fact per pass would be a fact per minute per machine that says nothing. The report carries the declaration it applied and what changed, so the control plane has what it needs to speak only when there is something to say.

A refusal is a fact with a subject in it — which machine, which resource, and the reason as the host gave it. A refusal that names only the machine is the silence this record is about, one level up.

Reports stay where they are. A node's report remains control traffic that only the control plane reads. The deploy facts are derived from it, which makes them second-hand on purpose: one emitter, one ordering, and no widening of the narrowest account in the mesh.

Options considered

  1. Leave it as it is, and let whatever cares ask the control plane. Rejected: asking for a fact that already arrives is the shape the mesh removed everywhere else, and nothing can react at the moment a machine changes — which is exactly when a graph, an audit or an operator wants to know.
  2. Each node emits its own facts. Rejected: it widens every host's account to an event namespace, and a host's authority is deliberately the narrowest in the mesh. Its report already reaches the one thing that can speak for it.
  3. Widen who may read the control branch. Rejected: that branch carries what machines say to the control plane, enrolment included. Widening its readers widens that too, for an unrelated reason.
  4. A registry of deploy hooks — something registers interest and is called. Rejected: an event is already the mechanism; there is nothing to register, and a callback is an address the mesh spent issue 102 learning not to keep.
  5. Put the facts on the node's own declaration stream. Rejected: that stream is last-per-subject by design, so a machine away for an hour gets exactly the current declaration and nothing older. A history of what happened cannot live in a stream built to forget.

Consequences

The audit logger gets the deploy half for nothing, because it consumes everything.

A failure becomes visible where the mesh is watched rather than where someone happened to be looking. That answers the open question issue 133 left about a record the store refused.

The catch-up replay stops being a burst of events. With the control plane able to state its own facts, replaying history as if it were happening now is a choice rather than the only option — and the better shape is the question the catalogue is actually asking, answered once (design 33).

The facts are second-hand. The control plane says what a machine reported, so a machine that cannot reach the bus produces no fact at all. Absence is not health, and what a machine was last heard from stays the place that says so.

The events stream carries more. Bounded by emitting on change rather than on every pass, and each fact is small; the stream's own limits remain what keeps it finite.

How this is checked

  • The control plane's grant names exactly the subjects it emits, derived from its seat like every other principal's, and the composed user list is compared against a golden file — so a fact it cannot publish fails a test rather than a catalogue's replay.
  • A convergence that changed nothing emits nothing. A test with two identical reports and one expected fact, because the failure this guards against is a fact per minute per machine.
  • A refusal names its resource. A test where a host reports a failed resource and the emitted fact carries which one and why, not merely that something went wrong.
  • What the mesh emits is what something consumes. The subject a module declares it consumes derives to the subject the control plane publishes — the same agreement test that already keeps the controller's own subscriptions honest.

References

  • ADR 0041 — an event is a relationship, not a call
  • ADR 0126 — an event is addressed to its emitter, because the emitter's identity is the meaning
  • ADR 0121, ADR 0129 — a role's events belong to the role
  • ADR 0083 — a report is held for the store rather than lost
  • ADR 0133 — the gate whose failure this makes visible
  • 03-DESIGN/01-to-be/32-what-a-module-declares.md §6 — the lifecycle, which ends today at a report nobody else may read
  • Measured 2026-09-28: 45 minutes of builds recorded nowhere with the overview reporting health; a module at 338 restarts found by hand; the control plane's only emitted event refused by its own permissions