Every send but a person's push (every send through sendToEach: plans, releases, healers, rotations, secrets given) composes mesh-controller and mesh-host at the build the machine was last sent when the mesh's record of it is older by the builds' registration order (issue 219), and carries everything else. Nothing is refused and nothing is retried. A plan whose own build is the older one finds its gate superseded by the kept build (issue 352) and ends.
Exceptions: a gate's own put-back carries its modules back (sendScope.puttingBack, set in gateFailed and sendRollbacks). A sent build that failed its gate is never kept, so when a put-back's own send was refused, the next automatic send completes the put-back.
A warning condition core.<component>.<machine>.core-downgrade-kept says it. It names the two ways out: push <node> takes the older build, and a newer merge or rebuild <module> moves the record forward. It is cleared after any send that carries the record or does not carry the module, a person's push included (overTheBus.declare).
If the kept build's manifest is no longer in the build records, the send to that machine is refused and says why, as for a recorded module (ADR 0245). The other machines are still sent.
Why
On 2026-10-02 a plan sent the control node a controller older than the one running there, and the mesh went quiet for ten minutes. ADR 0185 took the outage out of it, and the race stayed open. The decision of 2026-10-11 on issue 201 closes the class without pinning the trigger. Review 90: the first version refused the whole send, and that wedged a machine after a gate put-back whose own send was refused.
Design limits chosen
"Runs" is read as "was last sent" (node.sent_builds), as the gate and ADR 0236's checks read it.
Order is the asked time, read from the module's 50 newest builds. A retry or rebuild of an old commit is asked anew and counts as newer, as registration does, so the guard does not stop that move. A build missing from the records is not proved older.
Only the controller and the node-engine are kept, not the node tools.
The node-engine half of the decision (record what each core resource was told) is issue 234's send record.
How it was tested
TestACoreModuleIsKeptOverAnOlderRecord (no store): the order, a failed gate, the put-back scope, unknown builds.
TestReplay201 (store): a plan's judged send goes, keeps c2 and composes its manifest, and raises the condition. The put-back carries it back. A send that carried c2 leaves the condition open, and a send carrying the record clears it. It fails with the keep disabled.
TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (store): the gate failed c2 and the record is back at c1. A healer's send goes and keeps nothing. It fails without the failed-gate rule.
sh merge-check.sh against a throwaway postgres 17: exit 0.
The two store tests are not yet listed in testdata/store-tests, because open PR #266 changes that file.
Fixes issue 201
What
Every send but a person's push (every send through sendToEach: plans, releases, healers, rotations, secrets given) composes mesh-controller and mesh-host at the build the machine was last sent when the mesh's record of it is older by the builds' registration order (issue 219), and carries everything else. Nothing is refused and nothing is retried. A plan whose own build is the older one finds its gate superseded by the kept build (issue 352) and ends.
Exceptions: a gate's own put-back carries its modules back (sendScope.puttingBack, set in gateFailed and sendRollbacks). A sent build that failed its gate is never kept, so when a put-back's own send was refused, the next automatic send completes the put-back.
A warning condition `core.<component>.<machine>.core-downgrade-kept` says it. It names the two ways out: `push <node>` takes the older build, and a newer merge or `rebuild <module>` moves the record forward. It is cleared after any send that carries the record or does not carry the module, a person's push included (overTheBus.declare).
If the kept build's manifest is no longer in the build records, the send to that machine is refused and says why, as for a recorded module (ADR 0245). The other machines are still sent.
Why
On 2026-10-02 a plan sent the control node a controller older than the one running there, and the mesh went quiet for ten minutes. ADR 0185 took the outage out of it, and the race stayed open. The decision of 2026-10-11 on issue 201 closes the class without pinning the trigger. Review 90: the first version refused the whole send, and that wedged a machine after a gate put-back whose own send was refused.
Design limits chosen
- "Runs" is read as "was last sent" (node.sent_builds), as the gate and ADR 0236's checks read it.
- Order is the asked time, read from the module's 50 newest builds. A retry or rebuild of an old commit is asked anew and counts as newer, as registration does, so the guard does not stop that move. A build missing from the records is not proved older.
- Only the controller and the node-engine are kept, not the node tools.
- The node-engine half of the decision (record what each core resource was told) is issue 234's send record.
How it was tested
- TestACoreModuleIsKeptOverAnOlderRecord (no store): the order, a failed gate, the put-back scope, unknown builds.
- TestReplay201 (store): a plan's judged send goes, keeps c2 and composes its manifest, and raises the condition. The put-back carries it back. A send that carried c2 leaves the condition open, and a send carrying the record clears it. It fails with the keep disabled.
- TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (store): the gate failed c2 and the record is back at c1. A healer's send goes and keeps nothing. It fails without the failed-gate rule.
- `sh merge-check.sh` against a throwaway postgres 17: exit 0.
- The two store tests are not yet listed in testdata/store-tests, because open PR #266 changes that file.
Fixes issue 201
On 2026-10-02 a plan sent the control node a controller older than the one
running there, whose seat row the newer build had widened; the older binary
refused the row and the mesh went quiet for ten minutes. ADR 0185 took the
outage out of it, and the race stayed open.
Every send but a person's push now refuses a move of mesh-controller or
mesh-host to a build asked before the one its machine was last sent (the
builds' registration order, issue 219), whatever composed it — gated or not.
A gate's own put-back is the one exception, for the modules it puts back,
and says so in its send's scope. The refusal is the send's error, which the
plan keeps as its note, and a warning condition on the machine, cleared by
the next send there that carries no such move.
Deliverynovox/mesh-controller@b3787e4d8eb2 — delivered since 2026-10-12T18:57:32Z
Delivery plan — builds mesh-controller → novox; no bus step
tier 0: mesh-controller
novox: receives mesh-controller
Machines
mesh-controller on novox: passed — healthy 3 times over 3m42s
mesh-controller on the rest running it: sent
Transitions
2026-10-12 18:39 (new) → proposed (announced): novox/mesh-controller#273's head announced
2026-10-12 18:46 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-12 18:46 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-12 18:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-12 18:52 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
2026-10-12 18:57 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-controller@b3787e4d8eb2` — **delivered** since 2026-10-12T18:57:32Z
**Delivery plan** — builds mesh-controller → novox; no bus step
- tier 0: mesh-controller
- novox: receives mesh-controller
**Machines**
- mesh-controller on novox: passed — healthy 3 times over 3m42s
- mesh-controller on the rest running it: sent
**Transitions**
- 2026-10-12 18:39 (new) → proposed (announced): novox/mesh-controller#273's head announced
- 2026-10-12 18:46 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-12 18:46 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-12 18:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-12 18:52 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
- 2026-10-12 18:57 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Merge check at 9038d4b0
Judged: head 9038d4b0d317 merged into trunk f10aedac5880; read beside it: mesh-catalog 23ea0f431faf, mesh-controller-main f10aedac5880, mesh-host a1f9856b3957, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-controller → novox; no bus step
tier 0: mesh-controller
novox: receives mesh-controller
read by no module's build: cmd/mesh-controller/issue201_test.go
Run by the build seat on ace as build-1791822107369334674 (builds --log build-1791822107369334674).
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T16:17:25Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T16:17:25Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.049s
ok github.com/novox/mesh-controller/cmd/mesh-controller 242.027s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.014s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.019s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.013s
ok github.com/novox/mesh-controller/examples/route-proxy 1.124s
ok github.com/novox/mesh-controller/internal/artifacts 1.059s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 3.915s
ok github.com/novox/mesh-controller/internal/builder 35.190s
ok github.com/novox/mesh-controller/internal/catalogue 7.337s
ok github.com/novox/mesh-controller/internal/conditions 2.569s
ok github.com/novox/mesh-controller/internal/envfile 1.059s
ok github.com/novox/mesh-controller/internal/facts 1.068s
ok github.com/novox/mesh-controller/internal/identity 4.314s
ok github.com/novox/mesh-controller/internal/inventory 89.508s
ok github.com/novox/mesh-controller/internal/lease 17.701s
ok github.com/novox/mesh-controller/internal/licences 3.894s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.055s
ok github.com/novox/mesh-controller/internal/link 59.452s
ok github.com/novox/mesh-controller/internal/lint 2.244s
ok github.com/novox/mesh-controller/internal/outward 1.084s
ok github.com/novox/mesh-controller/internal/overlay 1.072s
ok github.com/novox/mesh-controller/internal/secrets 1.448s
ok github.com/novox/mesh-controller/internal/store 3.371s
ok github.com/novox/mesh-controller/internal/testbus 1.216s
ok github.com/novox/mesh-controller/internal/token 1.016s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.38s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.38s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.36s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.37s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.32s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.40s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.28s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.46s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.39s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.42s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.39s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.34s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.35s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.37s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.43s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.45s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.45s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.24s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.24s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.22s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.23s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.24s)
**Merge check** at `9038d4b0`
Judged: head `9038d4b0d317` merged into trunk `f10aedac5880`; read beside it: mesh-catalog `23ea0f431faf`, mesh-controller-main `f10aedac5880`, mesh-host `a1f9856b3957`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-controller → novox; no bus step
- tier 0: mesh-controller
- novox: receives mesh-controller
- read by no module's build: cmd/mesh-controller/issue201_test.go
Run by the build seat on ace as `build-1791822107369334674` (`builds --log build-1791822107369334674`).
```
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T16:17:25Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T16:17:25Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.049s
ok github.com/novox/mesh-controller/cmd/mesh-controller 242.027s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.014s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.019s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.013s
ok github.com/novox/mesh-controller/examples/route-proxy 1.124s
ok github.com/novox/mesh-controller/internal/artifacts 1.059s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 3.915s
ok github.com/novox/mesh-controller/internal/builder 35.190s
ok github.com/novox/mesh-controller/internal/catalogue 7.337s
ok github.com/novox/mesh-controller/internal/conditions 2.569s
ok github.com/novox/mesh-controller/internal/envfile 1.059s
ok github.com/novox/mesh-controller/internal/facts 1.068s
ok github.com/novox/mesh-controller/internal/identity 4.314s
ok github.com/novox/mesh-controller/internal/inventory 89.508s
ok github.com/novox/mesh-controller/internal/lease 17.701s
ok github.com/novox/mesh-controller/internal/licences 3.894s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.055s
ok github.com/novox/mesh-controller/internal/link 59.452s
ok github.com/novox/mesh-controller/internal/lint 2.244s
ok github.com/novox/mesh-controller/internal/outward 1.084s
ok github.com/novox/mesh-controller/internal/overlay 1.072s
ok github.com/novox/mesh-controller/internal/secrets 1.448s
ok github.com/novox/mesh-controller/internal/store 3.371s
ok github.com/novox/mesh-controller/internal/testbus 1.216s
ok github.com/novox/mesh-controller/internal/token 1.016s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.38s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.38s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.36s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.37s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.32s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.40s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.28s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.46s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.39s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.42s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.39s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.34s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.35s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.37s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.43s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.45s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.45s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.24s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.24s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.22s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.23s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.24s)
```
Independent review for hq issue 201 (a push recreated the controller at a digest older than its seat row). The guard is well placed and its tests are good, but it wedges a machine in one state that the code itself produces, and the condition does not clear when a person acts. Details below.
What I ran (head 9038d4b, local clone; also merges cleanly onto main f10aeda)
go vet ./...: clean.
go test -race -count=1 -run 'TestReplay201|TestACoreModule|TestARefusedCoreDowngrade' -v ./cmd/mesh-controller/ against a throwaway Postgres 18 (MESH_TEST_POSTGRES): all 3 PASS, and TestReplay201 ran against the store (0.18s).
I disabled the guard (refuseCoreDowngrades returned nil, nil at once), and TestReplay201 FAILED with "a plan's send of the older controller to the machine running the newer is refused: ". So the test proves the guard.
sh merge-check.sh with MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines.
Statuses on the head: mesh/merge-gate success ("head 9038d4b0d3 merged into trunk f10aedac58 — pass … every machine composes"), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable. There are no reviews before this one.
Checklist
1. Legitimate rollbacks.
A gate's put-back (gateFailed, sendRollbacks, and through them failCarried/putBackBroken) carries puttingBack together with its modules. It is exempt only for the modules it puts back. That is correct.
A person's push (pushCommand) does not go through sendToEach/ungatedIn. The bus step returns early on scope.person. Neither is touched.
A revert merge is a new commit with a new build that is asked after the merge. buildAsked returns the newest recorded build of the commit, so the revert is judged newer. Issue 280's "standing build" registers the old artifacts at the new commit, so it is still judged newer.
The release backlog re-release goes through gatedSend with judged set, so the guard applies to it. See finding A.
2. Wedging (finding A, blocking).errCoreDowngrade is an ordinary step error, and advanceHeld keeps it as the plan's note with "— tried again" on every tick, without a bound. Only three things end it: a newer registered build, a person's push, or the machine's sent record changing.
The trouble is that the guard cannot tell the 201 race apart from a gate's put-back that was registered and then not sent. In gateFailed, RestoreModule(previous) runs first and the send follows. If that send is refused, the record is at the older build while the machine was sent the failed, newer-asked one. notBack says this already. The send can be refused because of errUngated from another build registered during the gate window (two merges a minute apart is exactly the 201 setting), errBusWaits, or a send error. TestReplay201's fixture is exactly this state: the record is restored to the build before, and the machine was sent the newer one.
Before this PR, the next automated send to that machine finished the put-back, because the older build is gated. After it, every non-person send to that machine is refused whole: the plans' first and rest sends, release plans, healers, rotations, secrets given, and the hub send. Each walk retries forever. Because only one walk runs at a time (ADR 0276), the open walk holds every other walk too. Nothing but a person or a new merge ends it.
Suggested fix: a build that failed its gate is never "ahead". Do not count a move as a downgrade when its From build has a recorded failed gate (inv.GateFailed on that build's id, or the gate record's rollback state NotRolledBack/RollingBack with previous == To). Add a test for that: gate failed, put-back send refused, then a healer send goes through.
3. Order (no blocking finding). The comparison uses asked time (or recorded time), which is the same order RegisterModule uses (built_asked <=). The two orders differ only for builds that are recorded but not registered. Off-trunk builds have distinct commits. Failed-gate builds lead to finding A. Builds outside the 50-newest window, or with no record, leave the move unproved and let it pass (fail open), which is the right direction.
I found no case where a genuinely newer trunk build is judged older. A plan retry or a rebuild of an old commit is asked anew, so it becomes "newer" by this order. That matches registration and the decision's wording ("by the build's registration order"), but it means the guard does not stop that kind of downgrade. A comment should say so.
Minor: "the build its machine runs" is implemented as the build it was last sent (SentBuilds), not the one it reports running. That is fine as a choice, but the doc comment and the condition should say "was sent".
4. Condition (finding B, should fix, plus wording).
B: the condition is cleared only inside ungatedIn, and ungatedIn is never reached by a person's act: pushCommand bypasses sendToEach, and scope.person returns before the check. So after the person does what the condition asks, the warning stays until some later automated send reaches that machine, which could be hours on a quiet machine. Clear it on the person's path as well, for example by running refuseCoreDowngrades' clear for the pushed machine(s) when the push carries no downgrade, or by clearing on any successful send to the node.
Wording: "Needs: decide which build of the controller the mesh should hold; the details say which." does not name an act. Name the two ways out. push <node> accepts the older build, because it sends the record. A newer merge or rebuild mesh-controller moves the record forward. Also consider saying in the explanation that every automated send to that machine is held until then (that is what the operator will notice), and whether Warning is the right severity for that.
5. Tests and checks: they pass, as listed above.
6. Statuses: all three are green and read.
Other notes (not blocking)
Switching ungatedIn to machineMoves(..., true) and then skipping f.gated(mv.Module, mv.To) gives the same waiting list as before. I checked this against moves().
When the bus holder is added only for its user list and would be downgraded, it is skipped with a log line and the condition is raised. That is consistent.
The node-tools runtime (ComponentNodeTools) is not guarded. That matches the decision, which names only the controller and the node-engine.
Verdict: request changes at 9038d4b0d317d27be0183354b3d4b73cd3b89b9c
Independent review for hq issue 201 (a push recreated the controller at a digest older than its seat row). The guard is well placed and its tests are good, but it wedges a machine in one state that the code itself produces, and the condition does not clear when a person acts. Details below.
## What I ran (head 9038d4b, local clone; also merges cleanly onto main f10aeda)
- `go vet ./...`: clean.
- `go test -race -count=1 -run 'TestReplay201|TestACoreModule|TestARefusedCoreDowngrade' -v ./cmd/mesh-controller/` against a throwaway Postgres 18 (MESH_TEST_POSTGRES): all 3 PASS, and TestReplay201 ran against the store (0.18s).
- I disabled the guard (`refuseCoreDowngrades` returned nil, nil at once), and TestReplay201 FAILED with "a plan's send of the older controller to the machine running the newer is refused: <nil>". So the test proves the guard.
- `sh merge-check.sh` with MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines.
- Statuses on the head: mesh/merge-gate success ("head 9038d4b0d317 merged into trunk f10aedac5880 — pass … every machine composes"), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable. There are no reviews before this one.
## Checklist
**1. Legitimate rollbacks.**
- A gate's put-back (`gateFailed`, `sendRollbacks`, and through them `failCarried`/`putBackBroken`) carries `puttingBack` together with its modules. It is exempt only for the modules it puts back. That is correct.
- A person's push (`pushCommand`) does not go through `sendToEach`/`ungatedIn`. The bus step returns early on `scope.person`. Neither is touched.
- A revert merge is a new commit with a new build that is asked after the merge. `buildAsked` returns the newest recorded build of the commit, so the revert is judged newer. Issue 280's "standing build" registers the old artifacts at the new commit, so it is still judged newer.
- The release backlog re-release goes through `gatedSend` with `judged` set, so the guard applies to it. See finding A.
**2. Wedging (finding A, blocking).** `errCoreDowngrade` is an ordinary step error, and `advanceHeld` keeps it as the plan's note with "— tried again" on every tick, without a bound. Only three things end it: a newer registered build, a person's push, or the machine's sent record changing.
The trouble is that the guard cannot tell the 201 race apart from **a gate's put-back that was registered and then not sent**. In `gateFailed`, `RestoreModule(previous)` runs first and the send follows. If that send is refused, the record is at the older build while the machine was sent the failed, newer-asked one. `notBack` says this already. The send can be refused because of `errUngated` from another build registered during the gate window (two merges a minute apart is exactly the 201 setting), `errBusWaits`, or a send error. TestReplay201's fixture is exactly this state: the record is restored to the build before, and the machine was sent the newer one.
Before this PR, the next automated send to that machine finished the put-back, because the older build is gated. After it, every non-person send to that machine is refused whole: the plans' first and rest sends, release plans, healers, rotations, secrets given, and the hub send. Each walk retries forever. Because only one walk runs at a time (ADR 0276), the open walk holds every other walk too. Nothing but a person or a new merge ends it.
Suggested fix: a build that failed its gate is never "ahead". Do not count a move as a downgrade when its From build has a recorded failed gate (`inv.GateFailed` on that build's id, or the gate record's rollback state NotRolledBack/RollingBack with `previous == To`). Add a test for that: gate failed, put-back send refused, then a healer send goes through.
**3. Order (no blocking finding).** The comparison uses asked time (or recorded time), which is the same order `RegisterModule` uses (`built_asked <=`). The two orders differ only for builds that are recorded but not registered. Off-trunk builds have distinct commits. Failed-gate builds lead to finding A. Builds outside the 50-newest window, or with no record, leave the move unproved and let it pass (fail open), which is the right direction.
I found no case where a genuinely newer trunk build is judged older. A plan retry or a `rebuild` of an old commit is asked anew, so it becomes "newer" by this order. That matches registration and the decision's wording ("by the build's registration order"), but it means the guard does not stop that kind of downgrade. A comment should say so.
Minor: "the build its machine runs" is implemented as the build it was last **sent** (`SentBuilds`), not the one it reports running. That is fine as a choice, but the doc comment and the condition should say "was sent".
**4. Condition (finding B, should fix, plus wording).**
- B: the condition is cleared only inside `ungatedIn`, and `ungatedIn` is never reached by a person's act: `pushCommand` bypasses `sendToEach`, and `scope.person` returns before the check. So after the person does what the condition asks, the warning stays until some later automated send reaches that machine, which could be hours on a quiet machine. Clear it on the person's path as well, for example by running `refuseCoreDowngrades`' clear for the pushed machine(s) when the push carries no downgrade, or by clearing on any successful send to the node.
- Wording: "Needs: decide which build of the controller the mesh should hold; the details say which." does not name an act. Name the two ways out. `push <node>` accepts the older build, because it sends the record. A newer merge or `rebuild mesh-controller` moves the record forward. Also consider saying in the explanation that every automated send to that machine is held until then (that is what the operator will notice), and whether Warning is the right severity for that.
**5. Tests and checks:** they pass, as listed above.
**6. Statuses:** all three are green and read.
## Other notes (not blocking)
- Switching `ungatedIn` to `machineMoves(..., true)` and then skipping `f.gated(mv.Module, mv.To)` gives the same `waiting` list as before. I checked this against `moves()`.
- When the bus holder is added only for its user list and would be downgraded, it is skipped with a log line and the condition is raised. That is consistent.
- The node-tools runtime (ComponentNodeTools) is not guarded. That matches the decision, which names only the controller and the node-engine.
Refusing the whole send wedged a machine whose gate put back the
controller and whose put-back send was refused: every later automatic
send there was refused, retried each tick, and held up every other walk.
A send now composes the controller and the node-engine at the build the
machine was last sent when the mesh's record is older, and carries the
rest; nothing is refused or retried, and a plan whose own build is the
older one ends with its gate superseded. A sent build that failed its
gate is never kept, so the next send completes a put-back. The condition
names the two ways out and clears on any send that carries the record,
a person's push included.
mesh-admin
changed title from Refuse a send that puts the controller or node-engine behind its machine's build (hq issue 201) to Keep the controller and node-engine at the build a machine was last sent over an older record (hq issue 201)2026-10-12 16:51:30 +00:00
Merge check at d1704912
Judged: head d17049125655 merged into trunk f8daec01d3f0; read beside it: mesh-catalog 23ea0f431faf, mesh-controller-main f8daec01d3f0, mesh-host a1f9856b3957, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-controller → novox; no bus step
tier 0: mesh-controller
novox: receives mesh-controller
read by no module's build: cmd/mesh-controller/issue201_test.go
Run by the build seat on g14 as build-1791823881989317567 (builds --log build-1791823881989317567).
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T16:48:14Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T16:48:14Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-controller
notes:
- cmd/mesh-controller/issue201_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.036s
ok github.com/novox/mesh-controller/cmd/mesh-controller 250.902s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.017s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.026s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.015s
ok github.com/novox/mesh-controller/examples/route-proxy 1.155s
ok github.com/novox/mesh-controller/internal/artifacts 1.054s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.000s
ok github.com/novox/mesh-controller/internal/builder 29.799s
ok github.com/novox/mesh-controller/internal/catalogue 7.125s
ok github.com/novox/mesh-controller/internal/conditions 2.435s
ok github.com/novox/mesh-controller/internal/envfile 1.030s
ok github.com/novox/mesh-controller/internal/facts 1.035s
ok github.com/novox/mesh-controller/internal/identity 4.241s
ok github.com/novox/mesh-controller/internal/inventory 82.260s
ok github.com/novox/mesh-controller/internal/lease 17.701s
ok github.com/novox/mesh-controller/internal/licences 3.885s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.012s
ok github.com/novox/mesh-controller/internal/link 58.193s
ok github.com/novox/mesh-controller/internal/lint 2.197s
ok github.com/novox/mesh-controller/internal/outward 1.039s
ok github.com/novox/mesh-controller/internal/overlay 1.026s
ok github.com/novox/mesh-controller/internal/secrets 1.423s
ok github.com/novox/mesh-controller/internal/store 3.268s
ok github.com/novox/mesh-controller/internal/testbus 1.189s
ok github.com/novox/mesh-controller/internal/token 1.018s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.35s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.35s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.33s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.33s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.31s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.35s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.26s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.39s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.39s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.34s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.36s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.43s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.36s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.56s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.33s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.36s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.34s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.35s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.37s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.42s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.38s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.21s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.20s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.30s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.21s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.21s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.20s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.20s)
**Merge check** at `d1704912`
Judged: head `d17049125655` merged into trunk `f8daec01d3f0`; read beside it: mesh-catalog `23ea0f431faf`, mesh-controller-main `f8daec01d3f0`, mesh-host `a1f9856b3957`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-controller → novox; no bus step
- tier 0: mesh-controller
- novox: receives mesh-controller
- read by no module's build: cmd/mesh-controller/issue201_test.go
Run by the build seat on g14 as `build-1791823881989317567` (`builds --log build-1791823881989317567`).
```
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T16:48:14Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T16:48:14Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-controller
notes:
- cmd/mesh-controller/issue201_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.036s
ok github.com/novox/mesh-controller/cmd/mesh-controller 250.902s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.017s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.026s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.015s
ok github.com/novox/mesh-controller/examples/route-proxy 1.155s
ok github.com/novox/mesh-controller/internal/artifacts 1.054s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.000s
ok github.com/novox/mesh-controller/internal/builder 29.799s
ok github.com/novox/mesh-controller/internal/catalogue 7.125s
ok github.com/novox/mesh-controller/internal/conditions 2.435s
ok github.com/novox/mesh-controller/internal/envfile 1.030s
ok github.com/novox/mesh-controller/internal/facts 1.035s
ok github.com/novox/mesh-controller/internal/identity 4.241s
ok github.com/novox/mesh-controller/internal/inventory 82.260s
ok github.com/novox/mesh-controller/internal/lease 17.701s
ok github.com/novox/mesh-controller/internal/licences 3.885s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.012s
ok github.com/novox/mesh-controller/internal/link 58.193s
ok github.com/novox/mesh-controller/internal/lint 2.197s
ok github.com/novox/mesh-controller/internal/outward 1.039s
ok github.com/novox/mesh-controller/internal/overlay 1.026s
ok github.com/novox/mesh-controller/internal/secrets 1.423s
ok github.com/novox/mesh-controller/internal/store 3.268s
ok github.com/novox/mesh-controller/internal/testbus 1.189s
ok github.com/novox/mesh-controller/internal/token 1.018s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.35s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.35s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.33s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.33s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.31s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.35s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.26s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.39s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.39s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.34s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.36s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.43s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.36s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.56s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.33s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.36s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.34s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.35s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.37s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.42s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.38s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.21s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.20s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.30s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.21s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.21s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.20s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.20s)
```
Re-review against review 90. Keeping the core module instead of refusing the send fixes what review 90 raised: a send is no longer blocked, and a build that failed its gate is no longer kept. But a send that keeps the core module while carrying a gate now lets that gate be superseded at once, so the machine's other new builds go unjudged. Details below.
go test -race on TestReplay201, TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend and TestACoreModuleIsKeptOverAnOlderRecord: all PASS against the store.
I removed the from.failedGate exemption, and TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend FAILED ("the failed controller was kept over the build put back"). I made coreKept return nothing, and TestReplay201 FAILED. So both tests prove their guard.
sh merge-check.sh with MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines.
Statuses on the head: mesh/merge-gate success (head merged into trunk f8daec0), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable.
Review 90's findings
A (machine blocked): fixed. A send keeps the core module and carries everything else. The only refusal left is a kept build whose manifest is missing, and it applies per machine. A sent build that failed its gate is never kept, so the next healer's send finishes a rollback whose send was refused. The new test proves it.
B (condition not cleared): fixed.settleCoreDowngrades runs in overTheBus.declare, which both a person's push (push.go:664) and every other send go through. A send that carries the mesh's record clears the condition.
Wording: fixed. The needs line names both ways out.
What ends a keep
I confirmed what moves a kept machine forward. A newer merge or rebuild registers a build asked later than the one the machine was last sent, so coreDowngrades stops finding it and the next send carries the record. A person's push <node> takes the older build. Nothing keeps a machine at the old build silently: the warning stays raised for the whole time.
New finding C (blocking): a send that keeps the core module supersedes its own gate
gatedSend takes its carried moves from machineMoves(..., true), which is computed from the record. Unlike ungatedIn, it does not leave out the moves the send keeps. So on a kept machine, the gate carries mesh-controller c2→c1 while the declaration keeps c2. After the send, SentBuilds says c2, and in judgeGatesupersededOn(mesh-controller, node) is true, which settles the whole gate as GateSuperseded at once ("nothing judged, nothing put back").
I proved it on the store with the core201 fixture and a throwaway test, which I did not commit:
gatedSend's carried moves: [{Module:mesh-controller Node:anchor From:c2bbbbbbbbbb To:c1aaaaaaaaaa}]
kept: map[mesh-controller:c2bbbbbbbbbb]
superseded=true: anchor was sent mesh-controller c2bbbbbb after this gate's c1aaaaaa: the build judged no longer runs there, and this judging is superseded by that send's
The consequence is the 201 scenario itself. The node-engine's plan sends its first machine (the control node, which keeps the newer controller) the new node-engine. The gate is superseded without judging anything, the new node-engine stays there unjudged, and the plan ends superseded instead of walking the rest. The same happens to every release plan's gated send and every plan whose first machine is the kept one, for as long as the record stays older. The comment "A plan whose own build is the older one finds its gate superseded … and ends" covers only the plan whose own build is the older controller. Every other module's gate on that machine is skipped as well, which goes against ADR 0236 (no build moves on a machine without a gate).
Fix: in gatedSend, leave out of moves each move the send keeps, with the same filter ungatedIn uses (recordedKept(keepingRecorded(ctx), …), matched on module and From). Then the gate judges only what the declaration changes. Test: on core201, add a rolling-out module move on anchor, run a gated send, and check that the carried moves leave out mesh-controller and that the gate is not superseded.
Minor
TestReplay201 and TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend need the store but are not listed in testdata/store-tests. Without a store they skip unseen, which is what issue 459 guards against. Please add both lines.
The bus step (a person's word for the bus) also keeps the core modules now, because sendKeeps applies to it too. That is fine, but the doc comment says "No send but a person's push".
Verdict: request changes at d17049125655568a90396626c682df23f25e58bd
Re-review against review 90. Keeping the core module instead of refusing the send fixes what review 90 raised: a send is no longer blocked, and a build that failed its gate is no longer kept. But a send that keeps the core module while carrying a gate now lets that gate be superseded at once, so the machine's other new builds go unjudged. Details below.
## What I ran (head d170491, throwaway Postgres 18)
- `go vet ./...`: clean.
- `go test -race` on TestReplay201, TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend and TestACoreModuleIsKeptOverAnOlderRecord: all PASS against the store.
- I removed the `from.failedGate` exemption, and TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend FAILED ("the failed controller was kept over the build put back"). I made `coreKept` return nothing, and TestReplay201 FAILED. So both tests prove their guard.
- `sh merge-check.sh` with MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines.
- Statuses on the head: mesh/merge-gate success (head merged into trunk f8daec0), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable.
## Review 90's findings
- A (machine blocked): **fixed.** A send keeps the core module and carries everything else. The only refusal left is a kept build whose manifest is missing, and it applies per machine. A sent build that failed its gate is never kept, so the next healer's send finishes a rollback whose send was refused. The new test proves it.
- B (condition not cleared): **fixed.** `settleCoreDowngrades` runs in `overTheBus.declare`, which both a person's push (push.go:664) and every other send go through. A send that carries the mesh's record clears the condition.
- Wording: **fixed.** The needs line names both ways out.
## What ends a keep
I confirmed what moves a kept machine forward. A newer merge or `rebuild` registers a build asked later than the one the machine was last sent, so `coreDowngrades` stops finding it and the next send carries the record. A person's `push <node>` takes the older build. Nothing keeps a machine at the old build silently: the warning stays raised for the whole time.
## New finding C (blocking): a send that keeps the core module supersedes its own gate
`gatedSend` takes its carried moves from `machineMoves(..., true)`, which is computed from the record. Unlike `ungatedIn`, it does not leave out the moves the send keeps. So on a kept machine, the gate carries `mesh-controller c2→c1` while the declaration keeps c2. After the send, `SentBuilds` says c2, and in `judgeGate` `supersededOn(mesh-controller, node)` is true, which settles the **whole** gate as `GateSuperseded` at once ("nothing judged, nothing put back").
I proved it on the store with the `core201` fixture and a throwaway test, which I did not commit:
```
gatedSend's carried moves: [{Module:mesh-controller Node:anchor From:c2bbbbbbbbbb To:c1aaaaaaaaaa}]
kept: map[mesh-controller:c2bbbbbbbbbb]
superseded=true: anchor was sent mesh-controller c2bbbbbb after this gate's c1aaaaaa: the build judged no longer runs there, and this judging is superseded by that send's
```
The consequence is the 201 scenario itself. The node-engine's plan sends its first machine (the control node, which keeps the newer controller) the new node-engine. The gate is superseded without judging anything, the new node-engine stays there unjudged, and the plan ends `superseded` instead of walking the rest. The same happens to every release plan's gated send and every plan whose first machine is the kept one, for as long as the record stays older. The comment "A plan whose own build is the older one finds its gate superseded … and ends" covers only the plan whose own build is the older controller. Every other module's gate on that machine is skipped as well, which goes against ADR 0236 (no build moves on a machine without a gate).
Fix: in `gatedSend`, leave out of `moves` each move the send keeps, with the same filter `ungatedIn` uses (`recordedKept(keepingRecorded(ctx), …)`, matched on module and `From`). Then the gate judges only what the declaration changes. Test: on `core201`, add a rolling-out module move on anchor, run a gated send, and check that the carried moves leave out mesh-controller and that the gate is not superseded.
## Minor
- TestReplay201 and TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend need the store but are not listed in `testdata/store-tests`. Without a store they skip unseen, which is what issue 459 guards against. Please add both lines.
- The bus step (a person's word for the bus) also keeps the core modules now, because `sendKeeps` applies to it too. That is fine, but the doc comment says "No send but a person's push".
mesh/merge-gate head f0fd7fe8868e merged into trunk a681227af373 — fail: builds mesh-controller → novox; no bus step; head f0fd7fe8868e does not merge clea…
mesh/repo-check head f0fd7fe8868e merged into trunk a681227af373 — fail: head f0fd7fe8868e does not merge cleanly into trunk a681227af373: in conflict test…
gatedSend took its judged moves from the record, so on a machine kept at
a newer controller the gate judged the controller moving back to the
record, read its own send's kept controller as a later build there, and
was superseded with nothing judged. The kept moves are now left out
there as in ungatedIn (withoutKept). The doc says the bus step keeps
too, and the three store tests are listed.
Merge check at f0fd7fe8
Judged: head f0fd7fe8868e merged into trunk ba685b17f5e1; read beside it: mesh-catalog 23ea0f431faf, mesh-controller-main ba685b17f5e1, mesh-host a1f9856b3957, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-controller → novox; no bus step
tier 0: mesh-controller
novox: receives mesh-controller
read by no module's build: cmd/mesh-controller/issue201_test.go, testdata/store-tests
Run by the build seat on shanks as build-1791825418229347732 (builds --log build-1791825418229347732).
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T17:09:24Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T17:09:24Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.039s
ok github.com/novox/mesh-controller/cmd/mesh-controller 288.409s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.045s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.063s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.051s
ok github.com/novox/mesh-controller/examples/route-proxy 1.246s
ok github.com/novox/mesh-controller/internal/artifacts 1.090s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.542s
ok github.com/novox/mesh-controller/internal/builder 53.062s
ok github.com/novox/mesh-controller/internal/catalogue 7.826s
ok github.com/novox/mesh-controller/internal/conditions 2.636s
ok github.com/novox/mesh-controller/internal/envfile 1.049s
ok github.com/novox/mesh-controller/internal/facts 1.059s
ok github.com/novox/mesh-controller/internal/identity 5.349s
ok github.com/novox/mesh-controller/internal/inventory 111.875s
ok github.com/novox/mesh-controller/internal/lease 17.716s
ok github.com/novox/mesh-controller/internal/licences 4.856s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.052s
ok github.com/novox/mesh-controller/internal/link 66.548s
ok github.com/novox/mesh-controller/internal/lint 2.385s
ok github.com/novox/mesh-controller/internal/outward 1.087s
ok github.com/novox/mesh-controller/internal/overlay 1.077s
ok github.com/novox/mesh-controller/internal/secrets 1.542s
ok github.com/novox/mesh-controller/internal/store 3.598s
ok github.com/novox/mesh-controller/internal/testbus 1.267s
ok github.com/novox/mesh-controller/internal/token 1.014s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.42s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.43s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.43s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.41s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.41s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.38s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (4.39s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.45s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.31s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.50s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.61s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.48s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.43s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.49s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.47s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.43s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.41s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.41s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.40s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.44s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.49s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.48s)
./cmd/mesh-controller --- PASS: TestReplay201 (0.37s)
./cmd/mesh-controller --- PASS: TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (0.36s)
./cmd/mesh-controller --- PASS: TestAGateJudgesTheOtherMovesBesideAKeptCore (0.38s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.27s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.28s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.35s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.28s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.26s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.26s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.26s)
**Merge check** at `f0fd7fe8`
Judged: head `f0fd7fe8868e` merged into trunk `ba685b17f5e1`; read beside it: mesh-catalog `23ea0f431faf`, mesh-controller-main `ba685b17f5e1`, mesh-host `a1f9856b3957`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-controller → novox; no bus step
- tier 0: mesh-controller
- novox: receives mesh-controller
- read by no module's build: cmd/mesh-controller/issue201_test.go, testdata/store-tests
Run by the build seat on shanks as `build-1791825418229347732` (`builds --log build-1791825418229347732`).
```
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T17:09:24Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T17:09:24Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.039s
ok github.com/novox/mesh-controller/cmd/mesh-controller 288.409s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.045s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.063s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.051s
ok github.com/novox/mesh-controller/examples/route-proxy 1.246s
ok github.com/novox/mesh-controller/internal/artifacts 1.090s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.542s
ok github.com/novox/mesh-controller/internal/builder 53.062s
ok github.com/novox/mesh-controller/internal/catalogue 7.826s
ok github.com/novox/mesh-controller/internal/conditions 2.636s
ok github.com/novox/mesh-controller/internal/envfile 1.049s
ok github.com/novox/mesh-controller/internal/facts 1.059s
ok github.com/novox/mesh-controller/internal/identity 5.349s
ok github.com/novox/mesh-controller/internal/inventory 111.875s
ok github.com/novox/mesh-controller/internal/lease 17.716s
ok github.com/novox/mesh-controller/internal/licences 4.856s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.052s
ok github.com/novox/mesh-controller/internal/link 66.548s
ok github.com/novox/mesh-controller/internal/lint 2.385s
ok github.com/novox/mesh-controller/internal/outward 1.087s
ok github.com/novox/mesh-controller/internal/overlay 1.077s
ok github.com/novox/mesh-controller/internal/secrets 1.542s
ok github.com/novox/mesh-controller/internal/store 3.598s
ok github.com/novox/mesh-controller/internal/testbus 1.267s
ok github.com/novox/mesh-controller/internal/token 1.014s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.42s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.43s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.43s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.41s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.41s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.38s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (4.39s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.45s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.31s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.50s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.61s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.48s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.43s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.49s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.47s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.43s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.41s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.41s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.40s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.44s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.49s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.48s)
./cmd/mesh-controller --- PASS: TestReplay201 (0.37s)
./cmd/mesh-controller --- PASS: TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (0.36s)
./cmd/mesh-controller --- PASS: TestAGateJudgesTheOtherMovesBesideAKeptCore (0.38s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.27s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.28s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.35s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.28s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.26s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.26s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.26s)
```
Re-review against review 91. Finding C (a send that keeps the core module superseded its own gate) is fixed, and the minor points are fixed too.
What I checked
C is fixed.withoutKept is used by both ungatedIn and gatedSend. It leaves out a move when the send keeps it at the build the machine was last sent (matched on module and From). Its keep context is the same one the send composes under (keepingRecorded, unless the context already keeps). A gate on a kept machine now judges only the other moves.
The plan whose own build is the older core.owns adds that move back, so its gate is superseded and the plan ends, which is the documented outcome.
ungatedIn. It now leaves out every kept move, not only those the grants step keeps. moves() lists only modules that roll out, and recordedKept skips those unless every build is kept, so the only extra moves left out are the kept core modules. Nothing else in the send changes.
The new test proves the fix. I removed the withoutKept call from gatedSend, and TestAGateJudgesTheOtherMovesBesideAKeptCore FAILED: the gate's moves included mesh-controller c2→c1 beside the app's move. With the call in place it passes.
Minor points from review 91. All three store tests are now listed in testdata/store-tests. The doc comment now says that the bus step keeps the core modules too.
go test -race on TestReplay201, TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend, TestAGateJudgesTheOtherMovesBesideAKeptCore and TestACoreModuleIsKeptOverAnOlderRecord: all PASS.
sh merge-check.sh with MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines. Its list of store tests shows all three issue 201 tests as PASS.
Statuses on the head: mesh/merge-gate success (head merged into trunk ba685b1, every machine composes), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable.
The findings from reviews 90 and 91 are all addressed: no machine is blocked, a rollback whose send was refused is finished by the next send, the warning clears on a person's push, and the gate's judging is intact.
Verdict: approve at f0fd7fe8868e01035286bb9a282c2aa9f30969e8
Re-review against review 91. Finding C (a send that keeps the core module superseded its own gate) is fixed, and the minor points are fixed too.
## What I checked
- **C is fixed.** `withoutKept` is used by both `ungatedIn` and `gatedSend`. It leaves out a move when the send keeps it at the build the machine was last sent (matched on module and `From`). Its keep context is the same one the send composes under (`keepingRecorded`, unless the context already keeps). A gate on a kept machine now judges only the other moves.
- **The plan whose own build is the older core.** `owns` adds that move back, so its gate is superseded and the plan ends, which is the documented outcome.
- **`ungatedIn`.** It now leaves out every kept move, not only those the grants step keeps. `moves()` lists only modules that roll out, and `recordedKept` skips those unless every build is kept, so the only extra moves left out are the kept core modules. Nothing else in the send changes.
- **The new test proves the fix.** I removed the `withoutKept` call from `gatedSend`, and TestAGateJudgesTheOtherMovesBesideAKeptCore FAILED: the gate's moves included `mesh-controller c2→c1` beside the app's move. With the call in place it passes.
- **Minor points from review 91.** All three store tests are now listed in `testdata/store-tests`. The doc comment now says that the bus step keeps the core modules too.
## Runs (head f0fd7fe, throwaway Postgres 18)
- `go vet ./...`: clean.
- `go test -race` on TestReplay201, TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend, TestAGateJudgesTheOtherMovesBesideAKeptCore and TestACoreModuleIsKeptOverAnOlderRecord: all PASS.
- `sh merge-check.sh` with MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines. Its list of store tests shows all three issue 201 tests as PASS.
- Statuses on the head: mesh/merge-gate success (head merged into trunk ba685b1, every machine composes), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable.
The findings from reviews 90 and 91 are all addressed: no machine is blocked, a rollback whose send was refused is finished by the next send, the warning clears on a person's push, and the gate's judging is intact.
Merge check at f0fd7fe8
Judged: head f0fd7fe8868e merged into trunk a681227af373
mesh/merge-gate: FAIL — head f0fd7fe886 does not merge cleanly into trunk a681227af3: in conflict testdata/store-tests (modules: mesh-controller)
mesh/repo-check: FAIL — head f0fd7fe886 does not merge cleanly into trunk a681227af3: in conflict testdata/store-tests
Delivery plan — builds mesh-controller → novox; no bus step
tier 0: mesh-controller
novox: receives mesh-controller
read by no module's build: cmd/mesh-controller/issue201_test.go, testdata/store-tests
Run by the build seat on shanks as build-1791829341724108644 (builds --log build-1791829341724108644).
head f0fd7fe8868e does not merge cleanly into trunk a681227af373: in conflict testdata/store-tests
**Merge check** at `f0fd7fe8`
Judged: head `f0fd7fe8868e` merged into trunk `a681227af373`
- `mesh/merge-gate`: **FAIL** — head f0fd7fe8868e does not merge cleanly into trunk a681227af373: in conflict testdata/store-tests (modules: mesh-controller)
- `mesh/repo-check`: **FAIL** — head f0fd7fe8868e does not merge cleanly into trunk a681227af373: in conflict testdata/store-tests
**Delivery plan** — builds mesh-controller → novox; no bus step
- tier 0: mesh-controller
- novox: receives mesh-controller
- read by no module's build: cmd/mesh-controller/issue201_test.go, testdata/store-tests
Run by the build seat on shanks as `build-1791829341724108644` (`builds --log build-1791829341724108644`).
```
head f0fd7fe8868e does not merge cleanly into trunk a681227af373: in conflict testdata/store-tests
```
mesh/merge-gate head b3787e4d8eb2 merged into trunk a681227af373 — pass: builds mesh-controller → novox; no bus step; every machine composes with the chang…
Re-check at b3787e4d8e (merge of main a681227a into the approved f0fd7fe8): the PR's own diff against main is line-for-line the approved diff (diff of git diff merge-base..f0fd7fe8 and git diff a681227a..b3787e4d shows no differing +/- lines); only testdata/store-tests context moved. Review 94's approval carries to this head.
Re-check at b3787e4d8eb2226909ceebac414ea5b78ae8fbda (merge of main a681227a into the approved f0fd7fe8): the PR's own diff against main is line-for-line the approved diff (diff of `git diff merge-base..f0fd7fe8` and `git diff a681227a..b3787e4d` shows no differing +/- lines); only testdata/store-tests context moved. Review 94's approval carries to this head.
Merge check at b3787e4d
Judged: head b3787e4d8eb2 merged into trunk a681227af373; read beside it: mesh-catalog 22071dabfc7b, mesh-controller-main a681227af373, mesh-host 303d4037e84e, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-controller → novox; no bus step
tier 0: mesh-controller
novox: receives mesh-controller
read by no module's build: cmd/mesh-controller/issue201_test.go, testdata/store-tests
Run by the build seat on ace as build-1791830393099375212 (builds --log build-1791830393099375212).
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T18:27:33Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T18:27:33Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-controller
notes:
- cmd/mesh-controller/issue201_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.036s
ok github.com/novox/mesh-controller/cmd/mesh-controller 245.167s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.043s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.048s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.038s
ok github.com/novox/mesh-controller/examples/route-proxy 1.168s
ok github.com/novox/mesh-controller/internal/artifacts 1.085s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 3.969s
ok github.com/novox/mesh-controller/internal/builder 32.836s
ok github.com/novox/mesh-controller/internal/catalogue 7.298s
ok github.com/novox/mesh-controller/internal/conditions 2.586s
ok github.com/novox/mesh-controller/internal/envfile 1.049s
ok github.com/novox/mesh-controller/internal/facts 1.043s
ok github.com/novox/mesh-controller/internal/identity 4.506s
ok github.com/novox/mesh-controller/internal/inventory 88.062s
ok github.com/novox/mesh-controller/internal/lease 17.699s
ok github.com/novox/mesh-controller/internal/licences 4.012s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.043s
ok github.com/novox/mesh-controller/internal/link 59.998s
ok github.com/novox/mesh-controller/internal/lint 2.243s
ok github.com/novox/mesh-controller/internal/outward 1.096s
ok github.com/novox/mesh-controller/internal/overlay 1.077s
ok github.com/novox/mesh-controller/internal/secrets 1.461s
ok github.com/novox/mesh-controller/internal/store 3.507s
ok github.com/novox/mesh-controller/internal/testbus 1.199s
ok github.com/novox/mesh-controller/internal/token 1.017s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.39s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.40s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.38s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.37s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.34s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (4.33s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.39s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.27s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.41s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.42s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.37s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.37s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.37s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.42s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.38s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.36s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.37s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.38s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.40s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.44s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.42s)
./cmd/mesh-controller --- PASS: TestReplay201 (0.31s)
./cmd/mesh-controller --- PASS: TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (0.31s)
./cmd/mesh-controller --- PASS: TestAGateJudgesTheOtherMovesBesideAKeptCore (0.31s)
./cmd/mesh-controller --- PASS: TestReplay401 (0.47s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.24s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.23s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.23s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.22s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.23s)
**Merge check** at `b3787e4d`
Judged: head `b3787e4d8eb2` merged into trunk `a681227af373`; read beside it: mesh-catalog `22071dabfc7b`, mesh-controller-main `a681227af373`, mesh-host `303d4037e84e`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-controller → novox; no bus step
- tier 0: mesh-controller
- novox: receives mesh-controller
- read by no module's build: cmd/mesh-controller/issue201_test.go, testdata/store-tests
Run by the build seat on ace as `build-1791830393099375212` (`builds --log build-1791830393099375212`).
```
--- the gate: mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T18:27:33Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T18:27:33Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-controller
notes:
- cmd/mesh-controller/issue201_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.036s
ok github.com/novox/mesh-controller/cmd/mesh-controller 245.167s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.043s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.048s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.038s
ok github.com/novox/mesh-controller/examples/route-proxy 1.168s
ok github.com/novox/mesh-controller/internal/artifacts 1.085s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 3.969s
ok github.com/novox/mesh-controller/internal/builder 32.836s
ok github.com/novox/mesh-controller/internal/catalogue 7.298s
ok github.com/novox/mesh-controller/internal/conditions 2.586s
ok github.com/novox/mesh-controller/internal/envfile 1.049s
ok github.com/novox/mesh-controller/internal/facts 1.043s
ok github.com/novox/mesh-controller/internal/identity 4.506s
ok github.com/novox/mesh-controller/internal/inventory 88.062s
ok github.com/novox/mesh-controller/internal/lease 17.699s
ok github.com/novox/mesh-controller/internal/licences 4.012s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.043s
ok github.com/novox/mesh-controller/internal/link 59.998s
ok github.com/novox/mesh-controller/internal/lint 2.243s
ok github.com/novox/mesh-controller/internal/outward 1.096s
ok github.com/novox/mesh-controller/internal/overlay 1.077s
ok github.com/novox/mesh-controller/internal/secrets 1.461s
ok github.com/novox/mesh-controller/internal/store 3.507s
ok github.com/novox/mesh-controller/internal/testbus 1.199s
ok github.com/novox/mesh-controller/internal/token 1.017s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.39s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.40s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.38s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.37s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.34s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (4.33s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.39s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.27s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.41s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.42s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.37s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.37s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.37s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.42s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.38s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.36s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.37s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.38s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.40s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.44s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.42s)
./cmd/mesh-controller --- PASS: TestReplay201 (0.31s)
./cmd/mesh-controller --- PASS: TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (0.31s)
./cmd/mesh-controller --- PASS: TestAGateJudgesTheOtherMovesBesideAKeptCore (0.31s)
./cmd/mesh-controller --- PASS: TestReplay401 (0.47s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.24s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.23s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.23s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.22s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.23s)
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Every send but a person's push (every send through sendToEach: plans, releases, healers, rotations, secrets given) composes mesh-controller and mesh-host at the build the machine was last sent when the mesh's record of it is older by the builds' registration order (issue 219), and carries everything else. Nothing is refused and nothing is retried. A plan whose own build is the older one finds its gate superseded by the kept build (issue 352) and ends.
Exceptions: a gate's own put-back carries its modules back (sendScope.puttingBack, set in gateFailed and sendRollbacks). A sent build that failed its gate is never kept, so when a put-back's own send was refused, the next automatic send completes the put-back.
A warning condition
core.<component>.<machine>.core-downgrade-keptsays it. It names the two ways out:push <node>takes the older build, and a newer merge orrebuild <module>moves the record forward. It is cleared after any send that carries the record or does not carry the module, a person's push included (overTheBus.declare).If the kept build's manifest is no longer in the build records, the send to that machine is refused and says why, as for a recorded module (ADR 0245). The other machines are still sent.
Why
On 2026-10-02 a plan sent the control node a controller older than the one running there, and the mesh went quiet for ten minutes. ADR 0185 took the outage out of it, and the race stayed open. The decision of 2026-10-11 on issue 201 closes the class without pinning the trigger. Review 90: the first version refused the whole send, and that wedged a machine after a gate put-back whose own send was refused.
Design limits chosen
How it was tested
sh merge-check.shagainst a throwaway postgres 17: exit 0.Fixes issue 201
Delivery
novox/mesh-controller@b3787e4d8eb2— delivered since 2026-10-12T18:57:32ZDelivery plan — builds mesh-controller → novox; no bus step
Machines
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
9038d4b0Judged: head
9038d4b0d317merged into trunkf10aedac5880; read beside it: mesh-catalog23ea0f431faf, mesh-controller-mainf10aedac5880, mesh-hosta1f9856b3957, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-controller → novox; no bus step
Run by the build seat on ace as
build-1791822107369334674(builds --log build-1791822107369334674).Verdict: request changes at
9038d4b0d3Independent review for hq issue 201 (a push recreated the controller at a digest older than its seat row). The guard is well placed and its tests are good, but it wedges a machine in one state that the code itself produces, and the condition does not clear when a person acts. Details below.
What I ran (head
9038d4b, local clone; also merges cleanly onto mainf10aeda)go vet ./...: clean.go test -race -count=1 -run 'TestReplay201|TestACoreModule|TestARefusedCoreDowngrade' -v ./cmd/mesh-controller/against a throwaway Postgres 18 (MESH_TEST_POSTGRES): all 3 PASS, and TestReplay201 ran against the store (0.18s).refuseCoreDowngradesreturned nil, nil at once), and TestReplay201 FAILED with "a plan's send of the older controller to the machine running the newer is refused: ". So the test proves the guard.sh merge-check.shwith MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines.9038d4b0d3merged into trunkf10aedac58— pass … every machine composes"), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable. There are no reviews before this one.Checklist
1. Legitimate rollbacks.
gateFailed,sendRollbacks, and through themfailCarried/putBackBroken) carriesputtingBacktogether with its modules. It is exempt only for the modules it puts back. That is correct.pushCommand) does not go throughsendToEach/ungatedIn. The bus step returns early onscope.person. Neither is touched.buildAskedreturns the newest recorded build of the commit, so the revert is judged newer. Issue 280's "standing build" registers the old artifacts at the new commit, so it is still judged newer.gatedSendwithjudgedset, so the guard applies to it. See finding A.2. Wedging (finding A, blocking).
errCoreDowngradeis an ordinary step error, andadvanceHeldkeeps it as the plan's note with "— tried again" on every tick, without a bound. Only three things end it: a newer registered build, a person's push, or the machine's sent record changing.The trouble is that the guard cannot tell the 201 race apart from a gate's put-back that was registered and then not sent. In
gateFailed,RestoreModule(previous)runs first and the send follows. If that send is refused, the record is at the older build while the machine was sent the failed, newer-asked one.notBacksays this already. The send can be refused because oferrUngatedfrom another build registered during the gate window (two merges a minute apart is exactly the 201 setting),errBusWaits, or a send error. TestReplay201's fixture is exactly this state: the record is restored to the build before, and the machine was sent the newer one.Before this PR, the next automated send to that machine finished the put-back, because the older build is gated. After it, every non-person send to that machine is refused whole: the plans' first and rest sends, release plans, healers, rotations, secrets given, and the hub send. Each walk retries forever. Because only one walk runs at a time (ADR 0276), the open walk holds every other walk too. Nothing but a person or a new merge ends it.
Suggested fix: a build that failed its gate is never "ahead". Do not count a move as a downgrade when its From build has a recorded failed gate (
inv.GateFailedon that build's id, or the gate record's rollback state NotRolledBack/RollingBack withprevious == To). Add a test for that: gate failed, put-back send refused, then a healer send goes through.3. Order (no blocking finding). The comparison uses asked time (or recorded time), which is the same order
RegisterModuleuses (built_asked <=). The two orders differ only for builds that are recorded but not registered. Off-trunk builds have distinct commits. Failed-gate builds lead to finding A. Builds outside the 50-newest window, or with no record, leave the move unproved and let it pass (fail open), which is the right direction.I found no case where a genuinely newer trunk build is judged older. A plan retry or a
rebuildof an old commit is asked anew, so it becomes "newer" by this order. That matches registration and the decision's wording ("by the build's registration order"), but it means the guard does not stop that kind of downgrade. A comment should say so.Minor: "the build its machine runs" is implemented as the build it was last sent (
SentBuilds), not the one it reports running. That is fine as a choice, but the doc comment and the condition should say "was sent".4. Condition (finding B, should fix, plus wording).
ungatedIn, andungatedInis never reached by a person's act:pushCommandbypassessendToEach, andscope.personreturns before the check. So after the person does what the condition asks, the warning stays until some later automated send reaches that machine, which could be hours on a quiet machine. Clear it on the person's path as well, for example by runningrefuseCoreDowngrades' clear for the pushed machine(s) when the push carries no downgrade, or by clearing on any successful send to the node.push <node>accepts the older build, because it sends the record. A newer merge orrebuild mesh-controllermoves the record forward. Also consider saying in the explanation that every automated send to that machine is held until then (that is what the operator will notice), and whether Warning is the right severity for that.5. Tests and checks: they pass, as listed above.
6. Statuses: all three are green and read.
Other notes (not blocking)
ungatedIntomachineMoves(..., true)and then skippingf.gated(mv.Module, mv.To)gives the samewaitinglist as before. I checked this againstmoves().Refuse a send that puts the controller or node-engine behind its machine's build (hq issue 201)to Keep the controller and node-engine at the build a machine was last sent over an older record (hq issue 201)Merge check at
d1704912Judged: head
d17049125655merged into trunkf8daec01d3f0; read beside it: mesh-catalog23ea0f431faf, mesh-controller-mainf8daec01d3f0, mesh-hosta1f9856b3957, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-controller → novox; no bus step
Run by the build seat on g14 as
build-1791823881989317567(builds --log build-1791823881989317567).Verdict: request changes at
d170491256Re-review against review 90. Keeping the core module instead of refusing the send fixes what review 90 raised: a send is no longer blocked, and a build that failed its gate is no longer kept. But a send that keeps the core module while carrying a gate now lets that gate be superseded at once, so the machine's other new builds go unjudged. Details below.
What I ran (head
d170491, throwaway Postgres 18)go vet ./...: clean.go test -raceon TestReplay201, TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend and TestACoreModuleIsKeptOverAnOlderRecord: all PASS against the store.from.failedGateexemption, and TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend FAILED ("the failed controller was kept over the build put back"). I madecoreKeptreturn nothing, and TestReplay201 FAILED. So both tests prove their guard.sh merge-check.shwith MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines.f8daec0), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable.Review 90's findings
settleCoreDowngradesruns inoverTheBus.declare, which both a person's push (push.go:664) and every other send go through. A send that carries the mesh's record clears the condition.What ends a keep
I confirmed what moves a kept machine forward. A newer merge or
rebuildregisters a build asked later than the one the machine was last sent, socoreDowngradesstops finding it and the next send carries the record. A person'spush <node>takes the older build. Nothing keeps a machine at the old build silently: the warning stays raised for the whole time.New finding C (blocking): a send that keeps the core module supersedes its own gate
gatedSendtakes its carried moves frommachineMoves(..., true), which is computed from the record. UnlikeungatedIn, it does not leave out the moves the send keeps. So on a kept machine, the gate carriesmesh-controller c2→c1while the declaration keeps c2. After the send,SentBuildssays c2, and injudgeGatesupersededOn(mesh-controller, node)is true, which settles the whole gate asGateSupersededat once ("nothing judged, nothing put back").I proved it on the store with the
core201fixture and a throwaway test, which I did not commit:The consequence is the 201 scenario itself. The node-engine's plan sends its first machine (the control node, which keeps the newer controller) the new node-engine. The gate is superseded without judging anything, the new node-engine stays there unjudged, and the plan ends
supersededinstead of walking the rest. The same happens to every release plan's gated send and every plan whose first machine is the kept one, for as long as the record stays older. The comment "A plan whose own build is the older one finds its gate superseded … and ends" covers only the plan whose own build is the older controller. Every other module's gate on that machine is skipped as well, which goes against ADR 0236 (no build moves on a machine without a gate).Fix: in
gatedSend, leave out ofmoveseach move the send keeps, with the same filterungatedInuses (recordedKept(keepingRecorded(ctx), …), matched on module andFrom). Then the gate judges only what the declaration changes. Test: oncore201, add a rolling-out module move on anchor, run a gated send, and check that the carried moves leave out mesh-controller and that the gate is not superseded.Minor
testdata/store-tests. Without a store they skip unseen, which is what issue 459 guards against. Please add both lines.sendKeepsapplies to it too. That is fine, but the doc comment says "No send but a person's push".Merge check at
f0fd7fe8Judged: head
f0fd7fe8868emerged into trunkba685b17f5e1; read beside it: mesh-catalog23ea0f431faf, mesh-controller-mainba685b17f5e1, mesh-hosta1f9856b3957, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-controller → novox; no bus step
Run by the build seat on shanks as
build-1791825418229347732(builds --log build-1791825418229347732).Verdict: approve at
f0fd7fe886Re-review against review 91. Finding C (a send that keeps the core module superseded its own gate) is fixed, and the minor points are fixed too.
What I checked
withoutKeptis used by bothungatedInandgatedSend. It leaves out a move when the send keeps it at the build the machine was last sent (matched on module andFrom). Its keep context is the same one the send composes under (keepingRecorded, unless the context already keeps). A gate on a kept machine now judges only the other moves.ownsadds that move back, so its gate is superseded and the plan ends, which is the documented outcome.ungatedIn. It now leaves out every kept move, not only those the grants step keeps.moves()lists only modules that roll out, andrecordedKeptskips those unless every build is kept, so the only extra moves left out are the kept core modules. Nothing else in the send changes.withoutKeptcall fromgatedSend, and TestAGateJudgesTheOtherMovesBesideAKeptCore FAILED: the gate's moves includedmesh-controller c2→c1beside the app's move. With the call in place it passes.testdata/store-tests. The doc comment now says that the bus step keeps the core modules too.Runs (head
f0fd7fe, throwaway Postgres 18)go vet ./...: clean.go test -raceon TestReplay201, TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend, TestAGateJudgesTheOtherMovesBesideAKeptCore and TestACoreModuleIsKeptOverAnOlderRecord: all PASS.sh merge-check.shwith MESH_TEST_POSTGRES: exit 0, with no FAIL and no NOT RUN lines. Its list of store tests shows all three issue 201 tests as PASS.ba685b1, every machine composes), mesh/repo-check success, mesh/delivery success ("ready"). The forge says it is mergeable.The findings from reviews 90 and 91 are all addressed: no machine is blocked, a rollback whose send was refused is finished by the next send, the warning clears on a person's push, and the gate's judging is intact.
Merge check at
f0fd7fe8Judged: head
f0fd7fe8868emerged into trunka681227af373mesh/merge-gate: FAIL — headf0fd7fe886does not merge cleanly into trunka681227af3: in conflict testdata/store-tests (modules: mesh-controller)mesh/repo-check: FAIL — headf0fd7fe886does not merge cleanly into trunka681227af3: in conflict testdata/store-testsDelivery plan — builds mesh-controller → novox; no bus step
Run by the build seat on shanks as
build-1791829341724108644(builds --log build-1791829341724108644).Re-check at
b3787e4d8e(merge of maina681227ainto the approvedf0fd7fe8): the PR's own diff against main is line-for-line the approved diff (diff ofgit diff merge-base..f0fd7fe8andgit diff a681227a..b3787e4dshows no differing +/- lines); only testdata/store-tests context moved. Review 94's approval carries to this head.Merge check at
b3787e4dJudged: head
b3787e4d8eb2merged into trunka681227af373; read beside it: mesh-catalog22071dabfc7b, mesh-controller-maina681227af373, mesh-host303d4037e84e, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-controller)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-controller → novox; no bus step
Run by the build seat on ace as
build-1791830393099375212(builds --log build-1791830393099375212).