Building the bus: the decisions the work needed, and what it taught back #150

Merged
jschoubben merged 45 commits from feat/nats-genesis into main 2026-09-27 17:06:40 +00:00
2 changed files with 24 additions and 7 deletions
Showing only changes of commit 78a2274baf - Show all commits
+12 -3
View File
@@ -56,11 +56,20 @@ mesh.control.enrol an enrolment request (JetStream: CONTR
mesh.control.built a build's outcome (JetStream: CONTROL)
mesh.node.<node>.declare a declaration for a node (JetStream: NODES, last-per-subject)
mesh.build.request work for the build machine (JetStream: BUILDS, work queue)
mesh.events.<module>.<event> an event (JetStream: EVENTS)
mesh.tools.<module>.<tool> a tool invocation (core request/reply)
mesh.mod.<module>.event.<event> an event (JetStream: EVENTS)
mesh.mod.<module>.tool.<tool> a tool invocation (core request/reply)
mesh.seat.<seat>.accept.<verb> work submitted to a role (JetStream: per-seat work queue)
mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENTS)
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
mesh.ask.<node>.<command> the controller's command api (core request/reply)
```
**Revised 2026-09-26** ([design 29](29-what-a-module-declares.md)): a module's events and tools
moved from `mesh.events.*` / `mesh.tools.*` into one namespace per module, `mesh.mod.<module>.>`,
so a module's authority over its own name is a single subject pattern the server enforces — and
each carries a **kind token**, without which an events stream's filter would capture tool calls.
Seats are the same shape, one namespace per role.
Two things this buys over the exchanges: **request/reply is native** — a tool call is one
`request` on `mesh.tools.<module>.<tool>` answered by whichever runtime serves it (a queue group per
tool, so several nodes may serve one tool); and **a declaration is last-per-subject** — the NODES
@@ -93,7 +102,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
| CONTROL | `mesh.control.>` except `alive` | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
| BUILDS | `mesh.build.>` | work queue, explicit ack | at least once; a builder that dies mid-build has its message redelivered |
| EVENTS | `mesh.events.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
| EVENTS | `mesh.mod.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
call is a timeout the caller already handles.
@@ -34,11 +34,19 @@ the catalogue, and the mesh would have hundreds of copies of a decision it made
| declared | derived |
|---|---|
| `emits: order.placed` | publish on `mesh.mod.<module>.order.placed` |
| `consumes: billing.order.placed` | durable consumer on `mesh.mod.billing.order.placed` |
| `emits: order.placed` | publish on `mesh.mod.<module>.event.order.placed` |
| `consumes: billing.order.placed` | durable consumer on `mesh.mod.billing.event.order.placed` |
| `serves: status` | queue-group subscription on `mesh.mod.<module>.tool.status` |
| seat `telegram-sender`, `accepts: send` | work-queue consumer on `mesh.seat.telegram-sender.send` |
| `uses: telegram-sender` | publish on that seat's `accepts` subjects, and nothing else |
| seat `telegram-sender`, `accepts: send` | work-queue consumer on `mesh.seat.telegram-sender.accept.send` |
| `uses: telegram-sender` | publish on that seat's `accept` subjects, and nothing else |
**The `event` / `tool` / `accept` token is load-bearing, not decoration.** Revision, found while
defining the streams: a stream is defined by a subject filter, so a namespace holding both a
module's events and its tool calls cannot be filtered into an events stream without capturing
every tool invocation in the mesh — and a tool call must never be persisted
([design 25](25-the-bus-on-nats.md) §3 keeps tools on core NATS, where a lost call is a timeout the
caller already handles). The kind token is what makes `mesh.mod.*.event.>` a safe filter. The
first draft of this table had no token, which reads better and cannot be implemented.
**The test this must pass: the manifest survives the wire changing.** Reorganise the subject space
and every manifest in the catalogue is still correct. That is the property