Issue 170: assigning a module claims every seat it could hold

Assigning postgres on ace claimed mesh-store and made novox's own store
assignment unresolvable. The resolver reads a manifest's claims as 'does
hold'; ADR 0110 says the assignment holds, by a deliberate act the controller
already has (seat …, HoldSeat) but resolution does not consult.
This commit is contained in:
2026-09-30 14:29:11 +02:00
parent 846c1f85f2
commit d0044cf555
@@ -0,0 +1,63 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-controller internal/catalogue/resolve.go (holdings are derived from every resolved assignment's manifest `claims`)
- mesh-controller cmd/mesh-controller/seats.go (the deliberate act exists — HoldSeat, "recording … as its standing holder" — beside it)
fixed-by:
amended-design:
---
# 170 — Assigning a module claims every seat it could hold
## What was observed
ace's migration needs a postgres of its own: the operator's decision is that a `postgres` module
assigned on ace provides `postgres-database` to ace's modules and has **nothing to do with the
`mesh-store` seat**, which novox's assignment holds by a deliberate act already taken ("make
novox's postgres the mesh-store").
`assign ace postgres` (2026-09-30):
```
ace is assigned postgres
AND 1 other machine(s) cannot be worked out as things stand, so nothing will be sent to them:
novox
- postgres on novox claims "mesh-store", which postgres on ace already holds — one per mesh
mesh-controller: these assignments cannot be applied:
- postgres on ace claims "mesh-store", which postgres on novox already holds — one per mesh
```
The second assignment did not merely fail: it made **the control plane's own store's
assignment unresolvable** until unassigned. Nothing was pushed; the state is restored.
## Why
`resolve.go` derives what a node holds from the manifest's `claims` of every module resolved on
it, so a claim in a definition is a claim by every assignment of that module. The deliberate
act ADR 0110 describes exists beside it — `seat …` records "X on Y as its standing holder"
(`HoldSeat`) — but resolution does not consult that record; it consults the manifests.
## What was decided
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
> **A definition says which seats a module *can* hold. An assignment says which it *does*
> hold.** The store module can hold `mesh-store`, and it may be assigned to every node. Exactly
> one of those assignments holds the seat, because that assignment said so.
The manifest's `claims` is being read as *does hold*.
## What would be right
Resolution takes the holder of a seat from the recorded holding (the seat's standing holder),
not from the manifests: a module whose definition can hold a seat is assignable anywhere, and only
the assignment recorded as holder claims it — with the refusal reserved for a second *recorded*
holder at the seat's scope. Assigning postgres to ace is then exactly what the operator said it
is: a database provider on ace, and no more.
## Until then
`postgres` cannot be assigned on any second node; ace's database windows (baserow, letta, n8n,
car-hunter, txt-game) wait on this.