Issue 298 and ADR 0246: a seat's new verb is promised before it is required
Design 33 said a seat's verbs change additively, and section 3 made each one a condition of holding, so a verb added in the controller and served from the catalogue could land in neither order. It happened three times on 2026-10-07. Section 7 now says the three steps and how each is checked.
This commit is contained in:
+2
-1
@@ -53,7 +53,8 @@ is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
|
|||||||
provision**, and its holder is then the mesh's answer for it when several modules provide it
|
provision**, and its holder is then the mesh's answer for it when several modules provide it
|
||||||
([ADR 0126](../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). The set, with who holds each
|
([ADR 0126](../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). The set, with who holds each
|
||||||
seat, is the overview of what a mesh has ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)).
|
seat, is the overview of what a mesh has ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)).
|
||||||
A seat carries **verbs** — the tools every holder must serve.
|
A seat carries **verbs** — the tools every holder must serve. A verb added to a held seat is first
|
||||||
|
promised as optional, then served, then required ([ADR 0246](../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)).
|
||||||
- **operator** — the person who runs the mesh, and the one the mesh talks to.
|
- **operator** — the person who runs the mesh, and the one the mesh talks to.
|
||||||
*Not:* ~~master~~ (hq)
|
*Not:* ~~master~~ (hq)
|
||||||
- **person** — any human, as against the mesh acting unattended: *a person's word* releases a held
|
- **person** — any human, as against the mesh acting unattended: *a person's word* releases a held
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-07
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 246. A seat's new verb is promised before it is required
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) made serving a seat's verbs a
|
||||||
|
condition of holding it. [Design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7 says
|
||||||
|
those verbs change "additively within a version". The controller's claim check enforces two things: a
|
||||||
|
holder serves every verb the seat promises, and a claim names no verb the seat does not promise.
|
||||||
|
|
||||||
|
A mesh seat is defined in the controller, and its holders live in the catalogue. On 2026-10-07 three
|
||||||
|
verbs were added this way: `checks` on `mesh-delivery`, `failed` on `node-service-manager`, and
|
||||||
|
`resolvers` and `links` on `node-uplink`. Each time, neither repository could merge first
|
||||||
|
([issue 298](../04-ISSUES/298-a-new-seat-verb-deadlocks-across-two-repositories/00-report.md)). The new
|
||||||
|
controller refused the old holder for not serving the verb. The old controller refused the new holder for
|
||||||
|
claiming a verb it did not promise. Every catalogue check between the two failed on a module nobody had
|
||||||
|
touched. The controller was unblocked by marking the added verb optional (mesh-controller #117, kept
|
||||||
|
optional in a stored seat row by #114).
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
1. **Drop the check that refuses an unpromised verb.** The catalogue could then land first. But the check
|
||||||
|
catches a misspelt verb that would otherwise be served to nobody. The controller's change would still
|
||||||
|
refuse every holder not yet rebuilt, so only one order opens.
|
||||||
|
2. **A new seat version for every added verb.** §7 already allows a version for a change that would
|
||||||
|
break a caller. Running two versions side by side to add one verb costs a set of subjects, grants and
|
||||||
|
a retirement for nothing a caller would notice.
|
||||||
|
3. **Land both repositories at once.** Nothing joins two checked pull requests into one merge, and the
|
||||||
|
mesh would have to run both builds at the same moment on every machine.
|
||||||
|
4. **Promise first, require later.** The controller promises the verb as optional. Then the holder
|
||||||
|
serves it. Then the controller makes it required.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Option 4. **A verb added to a seat whose holder lives in another repository lands in three steps, each
|
||||||
|
in its own pull request:**
|
||||||
|
|
||||||
|
1. **The controller promises it, optional.** A claim that serves it is accepted, and a holder that does
|
||||||
|
not serve it still holds the seat. The mark comes from the seat as compiled and survives a seat row
|
||||||
|
read back from the store.
|
||||||
|
2. **Every holder serves it.** Each holder is checked against a controller that already promises the
|
||||||
|
verb.
|
||||||
|
3. **The controller requires it.** The mark is removed, and serving the verb becomes a condition of
|
||||||
|
holding like every other verb of the seat.
|
||||||
|
|
||||||
|
A verb on a new seat, or on a seat that nothing holds yet, is required from the start. Removing a verb
|
||||||
|
or changing its arguments in a way that breaks a caller is still a new version (§7).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Adding a verb to a held seat takes two controller changes and one in each holder's repository, where
|
||||||
|
design 33 implied one change. That is the price of §3 holding at every moment in between.
|
||||||
|
- Between steps 1 and 3, a caller can be told that a holder does not answer the verb. The caller finds
|
||||||
|
this out from the holder, not from the seat's protocol. Discovery lists the verb, because it is
|
||||||
|
promised.
|
||||||
|
- Nothing yet checks that step 3 happens. A verb left optional is a promise that no future holder has to
|
||||||
|
keep. Issue 298 is not resolved until a check covers it.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
- The controller's claim check refuses a claim that serves an unpromised verb, and a holder that misses a
|
||||||
|
required verb. It runs at registration, at handover, and in `module check`, which the catalogue's merge
|
||||||
|
check runs over every manifest with the controller the mesh runs.
|
||||||
|
- Tests in mesh-controller:
|
||||||
|
- `TestTheDeliverySeatPromisesChecks` (#117);
|
||||||
|
- `TestFailedIsAnOptionalVerbOfTheServiceManager`, `TestAnOptionalVerbStaysOptionalInASetReadFromTheStore`
|
||||||
|
and `TestASeatsVerbGainsTheArgumentsTheBinaryNames` (#114);
|
||||||
|
- `TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet` and
|
||||||
|
`TestTheUplinkVerbsReadBackFromTheRowStayOptional` (#116).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Issue 298](../04-ISSUES/298-a-new-seat-verb-deadlocks-across-two-repositories/00-report.md)
|
||||||
|
- [Design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §3 and §7
|
||||||
|
- mesh-controller #114, #116 and #117; mesh-catalog #105, #107 and #109
|
||||||
@@ -208,6 +208,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0237** — [A change is judged against the mesh that runs, before it merges, on the build seat](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)
|
- **0237** — [A change is judged against the mesh that runs, before it merges, on the build seat](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)
|
||||||
- **0238** — [A commit is the build at hand: one commit, one change plan, checked off the trunk and published only on it](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)
|
- **0238** — [A commit is the build at hand: one commit, one change plan, checked off the trunk and published only on it](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)
|
||||||
- **0239** — [A delivery is owned by the mesh-delivery module and runs from commit to delivered](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
- **0239** — [A delivery is owned by the mesh-delivery module and runs from commit to delivered](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
||||||
|
- **0246** — [A seat's new verb is promised before it is required](0246-a-seats-new-verb-is-promised-before-it-is-required.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-controller, mesh-tools]
|
code: [mesh-controller, mesh-tools]
|
||||||
updated: 2026-10-02
|
updated: 2026-10-07
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md
|
||||||
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
||||||
- 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
|
- 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
|
||||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
@@ -131,6 +132,24 @@ A seat's tools are an interface and change like one. Additive within a version.
|
|||||||
break a caller takes the version token the subject already has room for, and the two versions run side
|
break a caller takes the version token the subject already has room for, and the two versions run side
|
||||||
by side until nothing is bound to the old one.
|
by side until nothing is bound to the old one.
|
||||||
|
|
||||||
|
*Amended 2026-10-07 ([ADR 0246](../../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md),
|
||||||
|
[issue 298](../../04-ISSUES/298-a-new-seat-verb-deadlocks-across-two-repositories/00-report.md)):*
|
||||||
|
**additive takes two steps when the seat and its holder are in different repositories.** That is the
|
||||||
|
case for every mesh seat whose holder is a catalogue module. §3 makes every promised verb a condition of
|
||||||
|
holding, and a claim may name no verb the seat does not promise. So a verb added to the seat in the
|
||||||
|
controller refuses today's holder, and a holder that serves it is refused by today's controller. Neither
|
||||||
|
change can land first. An added verb therefore lands in three steps, each its own pull request:
|
||||||
|
|
||||||
|
1. **The controller promises the verb as optional.** A holder that serves it is accepted, and one that
|
||||||
|
does not yet serve it still holds the seat. The mark comes from the seat as compiled, so a seat row
|
||||||
|
read back from the store keeps it.
|
||||||
|
2. **Every holder serves it**, checked against a controller that already promises it.
|
||||||
|
3. **The controller requires it.** The mark is removed, and the verb is a condition of holding like the
|
||||||
|
rest.
|
||||||
|
|
||||||
|
A verb on a seat that nothing holds yet is required from the start. A change that breaks a caller is
|
||||||
|
still a new version.
|
||||||
|
|
||||||
## How it is checked
|
## How it is checked
|
||||||
|
|
||||||
- **A holder missing a verb cannot take the seat.** One test per condition of holding, as the existing
|
- **A holder missing a verb cannot take the seat.** One test per condition of holding, as the existing
|
||||||
@@ -142,6 +161,16 @@ by side until nothing is bound to the old one.
|
|||||||
answer equals what the seats declare — if it needed a module up, it would not be a read.
|
answer equals what the seats declare — if it needed a module up, it would not be a read.
|
||||||
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
|
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
|
||||||
of the subject table.
|
of the subject table.
|
||||||
|
- **An added verb lands in steps, and each step is checked (§7).** The controller's claim check refuses
|
||||||
|
a claim that serves a verb the seat does not promise, and a holder that misses a required verb. It runs
|
||||||
|
at registration, at handover, and in `module check`, which the catalogue's merge check runs over every
|
||||||
|
manifest with the controller the mesh runs. So a step out of order fails the pull request that took
|
||||||
|
it. That an optional verb stays optional when read back from the store is held by mesh-controller's
|
||||||
|
`TestAnOptionalVerbStaysOptionalInASetReadFromTheStore` (#114), and that a seat's row gains a verb's
|
||||||
|
new arguments by `TestASeatsVerbGainsTheArgumentsTheBinaryNames` (#114). Each added verb has its own
|
||||||
|
test that it is promised and not yet required: `TestTheDeliverySeatPromisesChecks` (#117) and
|
||||||
|
`TestFailedIsAnOptionalVerbOfTheServiceManager` (#114). **Not yet checked:** that step 3 happens. A
|
||||||
|
verb left optional binds no future holder ([issue 298](../../04-ISSUES/298-a-new-seat-verb-deadlocks-across-two-repositories/01-diagnosis.md)).
|
||||||
|
|
||||||
## What is built, 2026-09-30
|
## What is built, 2026-09-30
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-07
|
||||||
|
located-in: [mesh-controller internal/catalogue (seats.go, verbs.go), mesh-catalog (the seats' holders)]
|
||||||
|
fixed-by: mesh-controller PR #117, mesh-controller PR #114
|
||||||
|
amended-design: 03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 298. A new seat verb deadlocks across two repositories
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
[Design 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7 says a seat's verbs are
|
||||||
|
"additive within a version". §3 says a module may not hold a seat unless it serves every verb the seat
|
||||||
|
declares. A mesh seat's verbs are defined in the controller, and the modules that hold it live in the
|
||||||
|
catalogue. Under those two rules, adding a verb cannot land in one step, in either order:
|
||||||
|
|
||||||
|
- **The controller first.** The new controller promises the verb. The holder in the catalogue does not
|
||||||
|
serve it yet, so the claim check refuses the holder ("claims … but does not serve …, which that
|
||||||
|
seat's protocol promises"). Every check of the catalogue run against that controller fails on a module
|
||||||
|
nobody touched.
|
||||||
|
- **The catalogue first.** The new holder's claim says it serves the verb. The controller the mesh runs
|
||||||
|
does not promise it, so the same check refuses the claim ("says it serves …, which that seat's
|
||||||
|
protocol does not promise"). The catalogue's merge check runs every manifest through the controller
|
||||||
|
the mesh runs, so the pull request cannot pass.
|
||||||
|
|
||||||
|
Each repository's change waits on the other's, and neither can merge first.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
On 2026-10-07 this happened three times, once for each verb added to a mesh seat that day:
|
||||||
|
|
||||||
|
| Seat | Verb | Controller | Catalogue |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mesh-delivery` | `checks` | mesh-controller #117 | mesh-catalog #109 |
|
||||||
|
| `node-service-manager` | `failed` (until then the systemd module's own tool) | mesh-controller #114 | mesh-catalog #105 |
|
||||||
|
| `node-uplink` | `resolvers`, `links` | mesh-controller #116 | mesh-catalog #107 |
|
||||||
|
|
||||||
|
Each pair was unblocked the same way. The controller's `Verb` gained an `Optional` mark (#117): an
|
||||||
|
optional verb is promised, so a claim that serves it is accepted, but it is not required, so a holder
|
||||||
|
that does not serve it still holds the seat. `failed` was first written as required and refused today's
|
||||||
|
holder. #114 made it optional and fixed a second hole. The mark was never stored, so a verb seeded into
|
||||||
|
a seat row came back required when the row was read. The working set now takes the mark from the
|
||||||
|
compiled seat. #116 adds the uplink verbs as optional from the start.
|
||||||
|
|
||||||
|
## Why it is a design issue
|
||||||
|
|
||||||
|
Design 33 says what may change (additively) but not how a change crosses the two repositories that
|
||||||
|
share a seat. §3's condition of holding, enforced as designed, makes "additive" impossible whenever the
|
||||||
|
seat's definition and its holder are in different repositories. That is the case for every mesh seat
|
||||||
|
whose holder is a catalogue module. The rule that settles it is now in §7, by
|
||||||
|
[ADR 0246](../../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md): the
|
||||||
|
controller first promises the verb as optional, the holder serves it, and the controller requires it.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
- The controller's claim check (`internal/catalogue/seats.go`) refuses both an unpromised verb and a
|
||||||
|
missing required one. It runs at registration, at handover, and in `module check`, which the
|
||||||
|
catalogue's merge check runs over every manifest.
|
||||||
|
- Tests in the controller:
|
||||||
|
- `TestTheDeliverySeatPromisesChecks` (#117);
|
||||||
|
- `TestFailedIsAnOptionalVerbOfTheServiceManager`, `TestAnOptionalVerbStaysOptionalInASetReadFromTheStore`
|
||||||
|
and `TestASeatsVerbGainsTheArgumentsTheBinaryNames` (#114);
|
||||||
|
- `TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet` and
|
||||||
|
`TestTheUplinkVerbsReadBackFromTheRowStayOptional` (#116, open).
|
||||||
|
|
||||||
|
The trail is in [01-diagnosis.md](01-diagnosis.md). This is a core issue: at resolution it names its
|
||||||
|
replay, or says in `replay-none:` why none is possible (ADR 0237).
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 298 — diagnosis
|
||||||
|
|
||||||
|
## 2026-10-07: the two refusals
|
||||||
|
|
||||||
|
Both refusals are in one function of the controller, the claim check in `internal/catalogue/seats.go`.
|
||||||
|
After scope and provision, it checks two things about a claim:
|
||||||
|
|
||||||
|
1. `unservedVerbs`: every verb the seat's protocol promises is among what the claimant serves, under
|
||||||
|
the claim's `serves` or among its own tools. This is design 33 §3 and ADR 0132.
|
||||||
|
2. `unpromised`: everything the claim's `serves` names is a verb the protocol promises. A verb nobody
|
||||||
|
promised is served to nobody, so this catches a typo before a caller finds it as a timeout.
|
||||||
|
|
||||||
|
The protocol of a mesh seat is compiled into the controller and seeded into the store. Its holders are
|
||||||
|
catalogue modules. Check 1 refuses the old holder against the new controller, and check 2 refuses the new
|
||||||
|
holder against the old controller. The catalogue's merge check runs `module check` over every manifest
|
||||||
|
with the controller the mesh runs (`MESH_GATE`), so either order fails a check on one side.
|
||||||
|
|
||||||
|
Ruled out:
|
||||||
|
|
||||||
|
- **Dropping check 2.** Without it, a claim could name verbs that nothing promises, and the typo it
|
||||||
|
catches would come back. It would also only open one order (catalogue first). The controller's change
|
||||||
|
would still refuse every holder that is not yet rebuilt.
|
||||||
|
- **One commit in both repositories.** The two repositories merge separately, each through its own
|
||||||
|
checked pull request (ADR 0238). Nothing joins two pull requests into one merge.
|
||||||
|
- **A new seat version (§7's second sentence).** A version is for a change that would break a caller,
|
||||||
|
and running two versions side by side for an added verb costs more than the verb.
|
||||||
|
|
||||||
|
## 2026-10-07: the mark, and the store
|
||||||
|
|
||||||
|
`Verb.Optional` (mesh-controller #117) makes check 1 skip a verb that is marked, while check 2 still
|
||||||
|
counts it as promised. With the mark, the controller lands first and the old holder still holds the seat.
|
||||||
|
The holder that serves the verb then lands against a controller that promises it. Once every holder
|
||||||
|
serves it, removing the mark makes it a condition of holding like any other verb.
|
||||||
|
|
||||||
|
#114 found two holes in the store. First, re-seeding a seat row added a verb but never a new argument to
|
||||||
|
an existing verb, so the journal window's arguments would never have reached the row the mesh MCP server
|
||||||
|
reads. Second, the mark has `json:"-"` and is never stored, so a seeded verb came back required and the
|
||||||
|
deadlock returned on any mesh whose seat rows already existed. The working set now takes the mark from
|
||||||
|
the compiled seat (`optionalAsCompiled`), and a row's verb gains the arguments the binary names.
|
||||||
|
|
||||||
|
## What is still not checked
|
||||||
|
|
||||||
|
Nothing checks that the mark is ever removed. A verb that stays optional after every holder serves it is
|
||||||
|
a promise that no future holder has to keep. The design now names the third step. A check that fails
|
||||||
|
when a holder of the mesh already serves an optional verb, or one that fails after a date, would close
|
||||||
|
this. Both are left to the controller, and are the reason this issue stays `located`.
|
||||||
Reference in New Issue
Block a user