Merge remote-tracking branch 'origin/main' into fix/271-channel-reads-open-state-before-it-speaks

# Conflicts:
#	03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
This commit is contained in:
jochen
2026-10-12 21:09:11 +02:00
11 changed files with 334 additions and 30 deletions
+6 -2
View File
@@ -305,13 +305,17 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
**Purpose.** To take one commit from its pull request to every node that should run it, and to put it
back when it fails. **Recorded by** the `mesh-delivery` module (deliveries) and the controller (the
planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276.
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276, 0323.
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
(`mesh/merge-gate`, the build seat's composed check of the modules the change touches) and the
**repository check** (`mesh/repo-check`, the repository's own `merge-check.sh`)
(ADR 0238).
(ADR 0238). Both judge the merge of the pull request's head into a named **trunk commit**, made in the
build seat's own clone and never pushed (ADR 0323).
- **trunk commit** — the commit of the trunk a merge check's verdict was judged against: the trunk's head when
the build seat cloned it. The verdict holds for that commit only, and goes out of date when the trunk moves
(ADR 0323).
- **build seat** — the seat whose holder builds modules, `node-build-agent`; its holder on a node is the
**builder**. Any node may hold the seat, so there is no build node by nature
(ADR 0237).
+18 -8
View File
@@ -3,8 +3,9 @@ layer: to-be
status: in-progress
code:
- mesh-controller
updated: 2026-10-10
updated: 2026-10-12
decisions:
- "ADR 0315"
- "ADR 0001"
- "ADR 0005"
- "ADR 0006"
@@ -64,11 +65,20 @@ applications** — first-party, shipped with everything else, and running on the
anything else does. A task does not need to know a node exists, and *being ours does not make
something infrastructure*. `ai` is folded into `config`: a provider licence is an ordinary grant.
**`record` is an open question rather than an eighth entry.** Contexts integrate through it
(ADR 0008), which makes it load-bearing,
and *where it lives* is unresolved, tracked by issue 414 since
research 006 closed — putting it in the foundation risks recreating the circularity the tier design just
removed. Listing it here would settle by naming what has not been settled by arguing.
**`record` is not an eighth entry: it is the bus** (2026-10-12, ADR 0315). Contexts integrate through
it (ADR 0008), and where it lives was left open by research 006, for fear that putting it in the foundation
would recreate the circularity the tier design removed. ADR 0315 writes down what later records built: the
record is the bus's, the EVENTS stream, where each module says the events it declares (ADR 0041), and the
key-value buckets each module declares for its current state, which another module names in `reads` to read
(ADR 0201). Asking another context is a call to its verbs over the bus, request and reply, and is not the
record; a secret, and whatever a context keeps out of its buckets, is asked for. The controller's own records
are read from its record mirrors (ADR 0289) or asked of its verbs, never from its store. The record lives in
the foundation's bus, not in the store, which stays each context's private database reached only with that
context's own credential, so no store holds a record shared between contexts and the circularity does not
return. No module owns the record whole: each event and each bucket belongs to the module that declares it,
which alone writes it. *How it is checked:* where each rule was decided — the runtime's refusal of a bucket
not issued or a write to one only read (ADR 0201), one credential per context's store (ADR 0008), and the
mirrors' events counted live and compared with the store by probe D17 (ADR 0289).
**One of the seven is built.** `inventory` owns a database of that name and holds the node records;
the rest do not exist. What it takes to run any of them — the language, and what must already be
@@ -225,8 +235,8 @@ every public name.
- ~~**The contexts themselves.**~~ **Decided** — seven, by
ADR 0006.
What remains open is narrower and named there: **where the record lives**, which research 006
leaves unresolved because the foundation is the one place it must not go.
What remained open, **where the record lives**, is decided by ADR 0315: on the bus, in the EVENTS
stream and the buckets each module declares, never in a store (above).
- **How far it may be split.** One deployable today. Splitting a context out costs the single
interface a surface depends on
(research 011).
@@ -2,8 +2,11 @@
layer: to-be
status: in-progress
code: [mesh-controller internal/catalogue]
updated: 2026-10-08
updated: 2026-10-12
decisions:
- "ADR 0312"
- "ADR 0319"
- "ADR 0309"
- "ADR 0262"
- "ADR 0225"
- "ADR 0112"
@@ -151,6 +154,24 @@ ADR 0107).
never created, owned or removed by the mesh. The module requires read or read-write access. Where
the data is, is an operator value on the assignment.
*A read-write access writes through the path's own group (2026-10-12, ADR 0312, extending ADR 0051; the
build is tracked by issue 537):* when a module declares a `read-write` access, the node-engine reads the group
that owns the path as it applies, and makes the identity the module's code runs as a member of it: a
container's process gains it as a supplementary group, and an account that runs the module's tools is put in it
as ADR 0252 puts an account in a group. The module then writes the path as the path's group may, and as nothing
else. The path itself is never changed: no owner, group or mode is set on it or under it, and a read-write
access whose path's group cannot write is stated unhealthy, saying that the operator decides. A `read` access
grants no group. A path owned by group 0, or by a group that would let the identity act as root, is refused by
name. A group the node-engine granted is taken back when no declared read-write access asks for it any more,
never one the identity held before (ADR 0252 §3). A container is recreated with the group, and a service of
the machine's own service manager that runs the module's tools as an account is restarted once the account
holds it; a unit of the account's own service manager, or a login session, waits for a new login as ADR 0252
§5 and ADR 0254 say. The tool runner's account so gains the groups of every read-write access of the modules it
serves: the boundary is the account's, not the module's. *How it is checked:* the node-engine's tests ADR 0312
lists (the group given and the path's owner, group and mode unchanged; a read access granting nothing; an
unwritable group unhealthy and group 0 refused; the group taken back; the container recreated and the system
unit restarted holding the group).
**A port** is what ADR 0038 already decided: the
module says which port its software uses, and the node-engine answers with where the machine put it.
@@ -212,6 +233,26 @@ gains a setting by shipping its current value as the default, and `settings` lis
source: the default, the mesh, or the node. The first module to use it is the notifier, whose font size
and width are preferences. *How it is checked:* the table in ADR 0262.
*A value several modules share is set once (2026-10-12, ADR 0309, changing ADR 0164 §7 and §8 and ADR 0262
§3, §7, §8 and §10, and ADR 0319, changing ADR 0164 §4 and §9 and ADR 0309 §2, §3, §5 and §6; the build is
tracked by issue 415):* a module keeps every setting it takes, and a preference may also name a mesh-wide
**shared name**, such as `desktop.font-size`. Unset in every layer of its own module, the preference
reads the shared value, and only when that is unset too its default; read from the bottom, last
winning: the default, the shared value, the module's mesh-wide layer, the node's. So a person sets
the interface font size once and every module that names it follows, while any one module or node
can still be set apart in its own layer. A shared value is a fourth place of change beside the three
of ADR 0164 (a new default no longer reaches an assignment whose shared value is set), and the plan
of a change to it names every assignment, of every module that reads it,
whose effective value moves. It is set and cleared through the `settings` verb without a module, and
both `settings` listings name the shared name as a value's source beside the default, the mesh and the
node. **A shared value reaches no trusted setting** (ADR 0319): a trusted setting may not declare a
shared name, and registration refuses one that does, so a shared value is never trusted and needs no
warrant; a trusted setting is set in its own module's layer, on the operator's warrant (ADR 0277) or at
the terminal, as before. A trusted setting that a stored manifest still declares with a shared name
never reads the shared value, and both listings say why beside it. Whether a shared value is a
number, a colour or a string waits for the typed-settings work (issues 346 and 410). *How it is
checked:* the tables in ADR 0309 and ADR 0319.
*The operator's value travels only where it is asked for (2026-09-30,
[issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):*
a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking
@@ -12,8 +12,9 @@ code:
- mesh-tools src/main.ts
- mesh-catalog modules/mesh-catalog
- mesh-tools node-tools/internal/runtime (a module's state, ADR 0201)
updated: 2026-10-07
updated: 2026-10-12
decisions:
- "ADR 0307"
- "ADR 0240"
- "ADR 0233"
- "ADR 0201"
@@ -260,6 +261,17 @@ them on the bundle's channel — refusing, with the reason, a name it was not is
bucket it only reads. A watch hands the current values first, then every change: a bundle that starts
late, or starts again, has the whole of the state before it has any of the news.
*A key's kept values are read too (2026-10-12, ADR 0307, extending ADR 0201).* A bucket keeps as many past
values per key as its owner declared, and the runtime serves them: `history`, given a state and a key, answers
every value the bucket still keeps for that key, oldest first, each with its revision, whether it was a put or
a delete, and when it was written; an empty answer for a key it holds nothing for. It is a read like `get`:
served to the owner and to every reader, refused with the reason to a module the bucket was not issued to, and
it writes nothing. The Go SDK's `State(name)` carries it as `History(key)`, beside `Get`. Its first user is the
issue tracker's switch to its log (ADR 0300 §6), which carries every value its bucket still keeps; that work is
tracked by issue 502. *How it is checked:* the runtime's tests (several values and a delete answered oldest
first, with revision, operation and time; an empty answer for an unknown key; a reader served, a module the
bucket was not issued to refused, nothing written) and the SDK's tests of `History`.
The owner says how many past values a key keeps and how long a value lives, as a seat says how long
its backlog survives (§3); the mesh caps a value's size and a bucket's. **A bucket outlives its
module's assignment** — what a module stored is data
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-host, mesh-controller, mesh-catalog]
updated: 2026-10-09
updated: 2026-10-12
decisions:
- "ADR 0310"
- "ADR 0268"
- "ADR 0252"
- "ADR 0203"
@@ -88,6 +89,7 @@ WP4 the service manager's module, finished (mesh-catalog) independ
WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled
WP6 a module puts the account in a group (mesh-host, mesh-controller, mesh-catalog) issue 247
WP7 the control-node withholds execute (mesh-controller, mesh-catalog) ADR 0268
WP8 a node's files and accounts are read (mesh-sdk, mesh-catalog, mesh-controller) ADR 0310, issue 418
```
WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository
@@ -334,3 +336,33 @@ out, the control-node's value is set to `withhold` and zsh is pushed to it. Then
2. the control-node's `zsh_config` answers `execute` as not served, and says why;
3. `mesh_search` for `node-login-shell execute` names the other three nodes and not the control-node;
4. a call to the control-node's `node-login-shell.execute` is refused, and the other nodes still answer.
## WP8 — A node's files and accounts are read through `node-reads`
*`mesh-sdk`, `mesh-catalog`, `mesh-controller`. ADR 0310, extending ADR 0268; tracked by issue 418.*
ADR 0268 §5 has narrow read tools take the common reads over from `execute`, so that an approval is asked for an
act and not for a read, and ADR 0286 §44 sets the approval mark on `execute` only once those reads have a tool on
every node. Two had none: reading or searching a file, and looking up an account or a group.
- **A node seat, `node-reads`, on every node**, the control-node included, held by the tool runner; the
controller holds the seat's definition beside the other node seats.
- **Three reads:** a file read bounded in size, a search by fixed string bounded in matches and in depth, and a
lookup of an account or a group. Each verb says what it replaces (ADR 0245): `cat`, `grep`, `getent passwd`,
`getent group` and `id`. The bounds are set by the build and stated in each verb's description.
- **It reads as an account of its own**, which the mesh creates on every node, in no group that holds secrets,
with no login shell and no home of the operator's, so the node's file modes bound what it sees. `/proc` and
`/sys` are read as far as that account may.
- **Secret places are refused by name** before any read: the vault's directories, every module's own-secret
directory and the known credential files, whatever the file modes would allow.
- **One content rule.** The rule the operator channel applies to what it sends moves into `mesh-sdk`, and both
the operator channel and every `node-reads` answer apply it.
Once the seat answers on every node, the approval mark on `node-login-shell.execute` is set as ADR 0286 §44 says,
under the same issue.
**Proof.** The rows of ADR 0310's *how it is checked*: the controller's seat listing names a holder of
`node-reads` on every node; the tool runner's tests of the bounds, the account and the refusal by name; one
`mesh-sdk` test of the content rule and one in each of its users; `mesh_search` for `cat`, `grep` and
`getent passwd` finding the verbs. Live, the kernel's RAID status file is read through the seat on the home
server, and the agent's credentials file on the laptop is refused.
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-10-10
updated: 2026-10-12
decisions:
- "ADR 0303"
- "ADR 0215"
- "ADR 0173"
- "ADR 0040"
@@ -120,6 +121,20 @@ not write into its include directory. Each contribution is a dependency on the s
so assigning one of them without a window manager is refused. The first versions, which still write
the include files themselves, move to contributions once the controller derives the dependency.
**A package may be declared by several modules** (2026-10-11, ADR 0303, changing ADR 0210 §1, §10 and §11
for packages). Two modules on one node that each need a program may each declare its package, and it is
present while any of them declares it. Declaring a package asserts only that it is present: the node-engine
installs one the machine lacks, never upgrades it, and never removes it because a declaration went away, so a
second declaration changes nothing on the machine and taking one module off leaves the package to the others.
A module that needs a program another seat holder installs may declare that package itself, or still depend on
that seat. One module declaring a package present while another declares it absent (ADR 0180) on one node is
refused at assignment and at composition, naming both. A tool's configuration files and the contributions to
its seat keep their one owner, as above: only the package itself may be declared by several modules. *How it
is checked:* the controller's tests (two declarations of one package are assigned; present against absent is
refused, naming both modules) and the node-engine's tests ADR 0303 requires (a package declared twice is
installed once, one no module declares any more is not removed, an installed one is reported unchanged and not
upgraded).
**Added 2026-10-04.** `triggerhappy` holds `node-hotkeys` on both workstations (ADR 0212). The
laptop model's vendor keys become its contribution. The window-manager fragments of the launcher, the
clipboard, the wallpaper, the bar and the laptop model become `config` contributions to
@@ -4,7 +4,10 @@ status: in-progress
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
updated: 2026-10-12
decisions:
- "ADR 0325"
- "ADR 0323"
- "ADR 0318"
- "ADR 0314"
- "ADR 0293"
- "ADR 0289"
- "ADR 0285"
@@ -79,6 +82,7 @@ Every kind of state the core keeps, and the one component that writes it. Anyone
| a machine's declaration | controller (lease holder) | the bus, last per subject | read |
| a hand-over asked of a machine ([issue 356](../../04-ISSUES/356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel/00-report.md)) | controller, at its terminal | the machine, beside its state | the engine verifies the mesh's signature and records it, or refuses |
| a fresh setuid search asked of a machine ([issue 361](../../04-ISSUES/361-the-control-nodes-agent-account-is-never-judged/00-report.md)) | controller, at its terminal | the machine, which throws its last search away | the engine verifies the mesh's signature and starts it, or refuses |
| the take of a shared folder asked of a machine (ADR 0325 §8, issue 567; subject `mesh.node.<node>.ask.access-act`) | controller, on the operator's warrant | the machine, which records the act | the engine verifies the mesh's signature and takes it, or refuses |
| a machine's applied state and its report | the node-engine's apply queue (§6) | the machine; the report on the bus | the reconcile and a delivery *enqueue*, never apply |
| the controller lease | the controller instance holding it | key-value `mesh-controller_lease` | a candidate waits |
| plans and their tiers | controller (lease holder), compare-and-set on the plan's revision | the controller's store | read through `plans` |
@@ -551,6 +555,22 @@ the controller's replay R301 and its tests of the rule).
> its cause, so the exception is read from the verbs. Nothing decided changes: ADR 0230's acts stay
> exempt, and pushes by hand — what roll-out by default (ADR 0236) exists to end — still count.
**A condition no healer can fix becomes an issue** (2026-10-12, ADR 0314, extending ADR 0285; the build is
tracked by issue 409). A condition no healer can fix is one whose kind no healer answers, one a healer passes
over because it does not apply, or one handed to the operator after a healer's budget was spent. When such a
condition is raised, passed over or handed on, and no issue links its key (ADR 0285), the issue tracker opens
one itself, from the controller's condition events, with the condition's summary, its evidence and its key, and
links the key at once; the controller sets no link. Every message about the condition then names the issue
through its tracking line. Agent sessions take such an issue as any other: a claiming move that names the
worker, a branch, review and the required checks; no standing agent acts on a condition by itself. What an
agent does about it is written in the issue's notes, and every act it makes by hand on the mesh is also written
to the hand-act log, which S15 reads. ADR 0275 §9's week stands: the tracker opens no issue for a condition
before 2026-10-17 20:17 UTC, and until then such a condition is linked by whoever opens an issue for it. *How
it is checked:* the tracker's tests feeding condition events (one issue opened and linked for each of the three
cases, none for a condition a healer acts on or one already linked, none before the week ends); live, the
messenger's preview of such a condition says "Tracked as issue N", and the issue's notes and the hand-act log
hold what the agent did.
## 8. Staged core upgrades and rollback (rule 8)
**Health, per core component**, as probes in the registry (§4):
@@ -680,6 +700,19 @@ what is not an ordinary send, and the verdict — posted on the pull request. Th
a state machine, kept, followed by the release and written to the commit as a note; which parts are built
is Phase 5's.
Revision, ADR 0323 (2026-10-12). **The gate judges what merging makes, not the head.** The build seat takes
the trunk the pull request merges into, its head at that moment as the check's **trunk commit**, and makes in
its own clone the merge of the head into it, as the forge's default merge would; both layers judge that
merge, and a head that does not merge cleanly fails, naming the files in conflict. The merge is only checked:
never pushed, never registered. The verdict names the head and the trunk commit in both statuses' words
("head … merged into trunk …"), in mesh-delivery's `checks` and `show` and in the kept delivery plan, and
names the commits of the other repositories the check read; their move is issue 568's question. When the
trunk moves, the forge's holder puts both statuses of every open pull request into it back to pending with
the reason and runs nothing; the merge tool refuses a verdict whose trunk commit is not the trunk's head,
asks that head's check once, says how long the last check of the repository took, and never merges by
itself (checked by the builder's `TestReplay341`, replayed as R341, and the forge module's `trunk` and
`guard` tests).
**The replays.** mesh-lab carries a scripted scenario for each core incident, asserting the rule's
outcome, run on every merge to mesh-controller, `mesh-host` and mesh-tools:
@@ -901,4 +934,3 @@ in [to-be 47](47-delivery-from-commit-to-delivered.md), and their progress is tr
- Routing by presence, quiet hours, answering back through a channel, and an external dead-man
service — decided by ADR 0234,
designed in [to-be 46](46-the-conversation-with-the-operator.md).
- A condition that needs judgement handed to an agent as work — tracked by issue 409, since research 017 closed.
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog]
updated: 2026-10-10
updated: 2026-10-12
decisions:
- "ADR 0308"
- "ADR 0286"
- "ADR 0277"
- "ADR 0259"
@@ -289,6 +290,40 @@ Presence facts are events on the bus. The router keeps them as **current state o
machine and per intake kind, overwritten — never as a history, and never in a message's words. A
consumer that wants a timeline of the operator's day is a decision of its own.
### While a delivery walks
*2026-10-12, ADR 0308; the build is tracked by issue 532.*
A delivery restarts what it moves, and those restarts raise conditions that clear once it is done; a router
that restarted used to say again what it had been told to keep quiet (issue 531). So the router holds what a
walk touches and tells the walk's result once.
- **A walk names what it touches.** The controller's walk events say, when a walk starts and again when it
ends with its outcome (delivered, failed or stopped), the modules it moves and the machines it sends them to.
What it touches is those modules on those machines, and every module a moved tool runner or node-engine
carries there.
- **The router holds what an open walk touches.** A message whose subject is a touched module or machine is
held instead of sent, kept with when it was held, and `history` says it was held. A message several open
walks touch is held until the last of them ends or its bound passes.
- **Some messages are never held:** an ask (§6), a secret given or replaced, a message about anything no open
walk touches, and a walk's own failure.
- **A walk's end is told once:** one message with its outcome, the modules and machines it reached and how
long it took, then each held message whose condition is still open, one line each. One whose condition
cleared during the walk is only counted. A walk delivered with nothing held and nothing open is one line, or
nothing on a channel set to say only what needs the operator.
- **A hold has a bound:** twice the largest delivery budget in force among the walk's deliveries
([to-be 47](47-delivery-from-commit-to-delivered.md)): 20 minutes for a core module, 10 for leaf modules at
a five-minute budget and 6 at a three-minute one; 15 minutes, not doubled, for a walk none of whose
deliveries has a budget. Past it, every held message still open is sent with a line saying the walk still
runs and since when, and the cleared ones are counted. A walk that hangs never silences the mesh.
- **A restart says nothing it has said:** a router that restarts says again only what is open and was never
answered, never a silenced message or one withheld for its content.
*How it is checked:* the controller's tests of the walk events, and the router's tests of holding, never
holding, the end message, the bound and the restart, as ADR 0308 lists them; live, the next delivery of the
tool runner gives the operator one message at its end, and the channel's `history` shows what was held and
released.
### The content rule
The router's, applied before anything reaches any holder; §9 says what it allows. A `private` holder
@@ -2,8 +2,11 @@
layer: to-be
status: in-progress
code: [mesh-controller, mesh-catalog modules/mesh-delivery]
updated: 2026-10-11
updated: 2026-10-12
decisions:
- "ADR 0323"
- "ADR 0313"
- "ADR 0306"
- "ADR 0299"
- "ADR 0298"
- "ADR 0282"
@@ -79,7 +82,7 @@ allows.
| State | Means | Bound | H2 after the bound |
|---|---|---|---|
| proposed | a head was announced and is being checked | 1 hour | nothing: the check's own watchdog (S6) speaks |
| proposed | a head was announced and is being checked | 1 hour from when its check is asked; none while its verdict is out of date and no check is asked | nothing: the check's own watchdog (S6) speaks |
| checked | a verdict arrived | 1 minute | nothing: it is decided at once |
| ready | it passed; it waits to be merged | none | — |
| rejected | it failed its check | none | — |
@@ -88,7 +91,32 @@ allows.
| delivering | its walk runs; its builds are asked and registered tier by tier | 2 hours | `delivered`, `failed` or `superseded`, as the walk's record says |
| delivered, failed, superseded, stopped, retired, covered | final | — | — |
The transitions are those of ADR 0239 decision 2 and ADR 0270. A delivery that is
**A verdict holds for one trunk commit** (ADR 0323). The check judges the head merged into the trunk's head
at the moment the build seat clones it, the **trunk commit**, and the verdict names both. When another merge
moves the trunk, a `ready` delivery goes back to `proposed` on the observed event `trunk moved`, with both
commits in the transition's reason; a `rejected` one stays `rejected`, its statuses saying the verdict is out
of date. Nothing is asked for it: a recheck, a new head or the merge tool asks the check again. The row from
`proposed` to `checked` takes only a verdict that names the trunk's head as its trunk commit, so a verdict for
an older trunk is kept and shown and does not count, and the rows reach a fixed point. Checked by
mesh-delivery's `TestATrunkMoveSendsAReadyDeliveryBackToProposed` and the table's own walk.
What this leaves as it is, said so nobody reads more into it:
- **mesh-delivery's trunk head is the last merge it heard** into that trunk while the delivery was open. A push
to the trunk that is not a merge it hears does not move it; the forge's holder, which reads the branch, and
the merge tool, which compares commits when asked, still catch it.
- **A verdict that names no trunk commit**, from a build seat older than ADR 0323, counts in mesh-delivery as it
did: such a delivery stays `ready`. The forge reads the same verdict out of date once its trunk moves, and the
merge tool refuses it and asks the check. The two agree once the head is judged again.
- **A verdict no build judges** (a change that touches no module, in a repository with no `merge-check.sh` or
not the mesh's) names the base branch's head the forge announced with the head as its trunk commit.
- **In a busy repository the merge tool can refuse, ask, and refuse again**: another merge may move the trunk
while the asked check runs, and the new verdict is then out of date too. Each refusal asks once more; the
merge goes through once a check finishes before the next merge.
- **A check that was asked and never answers** is listed by `stalled` an hour after the ask, and is asked again
with `mesh-delivery.recheck`, which the merge tool's refusal names.
The transitions are those of ADR 0239 decision 2, ADR 0270 and ADR 0323. A delivery that is
merged while not `ready` goes to `held`. Its check did not pass, and only a person decides that it goes on. A
delivery merged with no walk answering it within ten minutes is held too, with the true reason, and listed by
`stalled` at once. Nothing waits silently.
@@ -102,7 +130,11 @@ delivery merged with no walk answering it within ten minutes is held too, with t
forge says that later merge commit contains it (`gitea_contains`, asked once per pair and kept). Merge
order is only the first filter, and a forge that cannot answer means not yet covered. `mesh/delivery`
says `covered: a later merge that contains it was delivered` as a success. A module that runs on one
machine only gets no "rest" step and is never covered;
machine only is covered too: its walk records the empty rest as sent once its first machine passes
(ADR 0270, as corrected 2026-10-12);
- *a stop whose walk ended without building its modules* (issue 474) stays `stopped`, a final state, and
is read as covered once a later merge covers it by the same test: `mesh/delivery` then says `covered after
its stop` with the covering merge, once, as a success. No state or transition is added (issue 480);
- the covered row is tried before `go`, from `held` and from `ready` past the ten minutes.
**History is retired, not held** (ADR 0250).
@@ -217,6 +249,14 @@ shown and held to no budget. Measured on 51 walks on 2026-10-09 and 10, a leaf w
(7 of 20 within five), a core walk of one tier 5.6 (worst 8.8), and a controller walk dragging two catalogue tiers
14.2 (issue 338).
*A wider measure, around the delivery budget (2026-10-12, ADR 0313, extending ADR 0282).* The delivery budget
above runs from the merge; the operator's own target is wider, the **fix time**, which runs from an issue's
first move to `fixing` to the moment its last fix is delivered. Each issue's class carries a **fix budget** too
(five minutes leaf, ten core, the delivery budget's own figures), and the tracker splits a fix time into four
phases — implementing, review, checks, and merge to running (the delivery time above) — so a fix over its
budget is said loud, naming its longest phase, and clears at the next fix of its class delivered within budget.
The delivery budget stays as written: a measure inside the fix budget, not replaced by it.
**What changes in the walk.**
- **A lone merge is cut at once**: no batch assembling, and no member of its delivery group still unmerged.
@@ -232,6 +272,32 @@ shown and held to no budget. Measured on 51 walks on 2026-10-09 and 10, a leaf w
- **Every phase is kept**: the window, the wait, the build, the send, each reading, the rest's first healthy
report; the plan shows an expected landing time from the last seven days' medians.
*Walks beside each other (2026-10-12, ADR 0306, extending ADR 0304, which narrowed the bullets above to what
a change shares).* Building the walks at once found what those bullets left open, and ADR 0306 decides it:
- **A walk's builds are its own, never another's backlog.** A send composes a module whose build another open
walk owns at the build its node was last sent, and neither carries nor judges it: that build is moved and
judged only by its own walk, so a failed walk puts back only its own builds. What waits on a node with no open
walk to move it is still backlog, carried and judged by the next gated send there (this changes ADR 0236 §17
for builds an open walk owns).
- **A delivery group still assembles.** A merge whose delivery group has a member not yet merged waits for the
merge window whether or not a batch is assembling, so a group's members walk in one batch, in their order.
- **Related means everything the walks would move.** A module in common is any module a walk plans to build or
send, the dependents of what its merges moved included; a change whose modules are not known yet is related to
every change. Two related walks are neither cut beside each other nor judged beside each other on one node.
- **A starved judgement is said.** A leaf judgement set aside by core builds three times in a row raises a
warning naming the walk and the core walks that set it aside.
- **A node's own fault fails every judgement on it.** A node found unwell as a whole (a failed apply, an
unhealthy network or engine) fails every judgement open on it, leaf and core, each walk putting back only its
own builds.
- **A rollback beside another walk's judgement** behaves as it does today; whether it should wait is tracked by
issue 528.
*How it is checked:* the controller's tests ADR 0306 lists, one per clause: an unsent build of walk A left out
of walk B's send and not taken up as backlog while A is open; a group's second member joining its first's batch;
overlapping dependents keeping two walks apart; the warning on the third set-aside; a failed apply failing every
open judgement on its node.
**Who builds what.**
| Part | Owner | What it adds |
@@ -268,9 +334,15 @@ only once it is built.
first, then *built by* and *version skew*, then *engine before controller*. The pair shows the rule that
won and the rules it won over. Rules of one rank both ways are a contradiction. The group is `rejected`,
and the reason names both members, both rules and the `after:` line that would settle it.
- **Check**: the members' heads composed together, one gate over every machine with all their definitions. It
is asked by mesh-delivery through `delivery-check` when the group forms or a member's head moves, and
posted on every member's head as `mesh/delivery-group`.
- **Check**: the members' heads composed together, one gate over every machine with all their definitions,
each head merged into its own trunk's head first and named with that trunk commit (ADR 0323). It is asked
by mesh-delivery through `delivery-check` when the group forms or a member's head moves, and posted on
every member's head as `mesh/delivery-group`. A member rebased or pushed is a new head with its own check:
it takes its old head's place in the group, even once the group is delivering, and the composed check runs
again for the new heads instead of holding the group. A merge into a member's trunk that is not one of the
group's members puts the composed check out of date: the group reads `checking` with the reason, and the
composed check runs again once a member is judged again. Checked by mesh-delivery's
`TestAGroupsComposedCheckGoesOutOfDateOnAMergeThatIsNotItsMember` and `TestARebasedMemberIsComposedAgainNotHeld`.
- **Delivering**: in order. A member waits in `published` until the one before is `delivered` or `covered`.
A member published while its group's composed check is asked and not answered waits there for the answer;
one held because that check had not passed is *lifted* back to `published` once it passes or warns for the
@@ -307,7 +379,7 @@ In this order, each by its owner:
| Verb | Answers or does |
|---|---|
| `deliveries` | every delivery not final, and the final ones of the last day, one line each; filtered by state, repository or group |
| `deliveries` | every delivery not final, the final ones of the last day, and every final one `stalled` still lists, one line each; filtered by state, repository or group |
| `show` | one delivery or group whole: plan, transitions, machine steps, order, the walk |
| `groups` | every group with its members in order and its derived state |
| `what-if` | the delivery plan a diffset would have, asked of the controller's planner, kept nowhere |
@@ -6,8 +6,9 @@ code:
- mesh-catalog modules/artifact-store-tools (the store's tools), modules/distribution (its unbuilt code removed)
- mesh-catalog modules/gitea (package retention)
- mesh-catalog modules/docker (image pruning)
updated: 2026-10-08
updated: 2026-10-12
decisions:
- "ADR 0311"
- "ADR 0251"
- "ADR 0189"
- "ADR 0257"
@@ -110,7 +111,8 @@ ADR 0257:
failed one, and the controller keeps a row per copy in `artifact_mirrored`. The copies the existing
build records already named were recorded by the migration that made the table.
- **Kept by the kept builds that stood on it** (`stood-on`), and by nothing else: a definition never
keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made.
keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. *(A copy of a pinned
third-party image is the exception, below: ADR 0311 §3.)*
- **Held whole.** A copy counts as held only when its index and every platform the index names are
held; otherwise it is copied again. A copy a build copies again is no longer recorded as let go of.
- **An index goes with its platforms only in a confirmed collect.** Before its first delete, it reads
@@ -122,6 +124,35 @@ ADR 0257:
`unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a
person names, and the sweep then decides about them.
### Pinned third-party images
*2026-10-12, ADR 0311, extending ADR 0257; the build is tracked by issue 421.* A third-party image a module
pins was held only upstream, and an image withdrawn there was found only when a node that lacked it tried to
deploy (issue 113). So the mesh holds a copy of what its modules pin, and checks both sources:
- **Every pinned third-party image is copied** into the artifact store when it is first used, into the
repository of its upstream image (`upstream/<host>/<path>`, as above).
- **Its record names the pin.** The copy's row names, as its reason, the module whose definition pinned the
digest (changing ADR 0257 §6, which named only a build or a person's reason).
- **Kept while a module pins it.** A copy is kept, with the reason `pinned`, while any module's current
definition pins its digest; neither the sweep after a build nor the nightly clean-up lets it go. For these
copies a definition does keep a copy, and one is eligible only when no kept build stood on it and no current
definition pins it (changing ADR 0257 §8, §9 and §20). `artifact_store_references` shows them with the reason
`pinned`.
- **A node pulls from the mesh first**, from the artifact store's copy, and from the upstream only second.
- **A scheduled check pulls every pinned image as an empty node would**, once from the copy and once from the
upstream. An upstream that no longer serves while the copy does raises a warning condition naming the module
and the image; neither serving raises an urgent one; each clears once the image is served again. An upstream
that serves while the copy does not is not decided yet (issue 421); a node then still pulls from the upstream.
- **A module may declare its upstream**, the object store's module first, with its maintained fork, and a view
shows each declared upstream's last release and open advisories, so ADR 0288's triggers of no release for six
months and of an advisory unpatched for 30 days can be read there.
*How it is checked:* the table in ADR 0311 — the controller's copy and its row, the collection test of keeping
by pin, the node-engine's pull from the store with the upstream unreachable, the check's two conditions, the
manifest test of a declared upstream, the view against a recorded answer, and live, the object store's pinned
image held in the store.
### The controller's verbs for the store
- **`artifacts`** reads only. It answers every reference the mesh recorded making, with its state and,
+24 -4
View File
@@ -2,9 +2,10 @@
layer: to-be
status: designed
code: []
updated: 2026-10-11
updated: 2026-10-12
decisions:
- "ADR 0290"
- "ADR 0321"
---
# 55 — The knowledge base
@@ -125,8 +126,17 @@ approval. On `approve`:
On acceptance the record and its clauses move to `accepted` and `in force`, every clause its `changes` and
`supersedes` name moves to `changed` or `superseded`, and `accepted` and `changed` events are emitted. Then the 4
rules that judge the whole record set run (an accepted decision cited by nothing, and the like); a failure raises a
condition naming the rule and the records, and does not undo the acceptance.
rules that judge the whole record set run (an accepted decision cited by nothing, and the like). A failure does not
undo the acceptance. It is a fault of the records, which their authors repair, not of the module: it is told to the
operator once, as a warning naming the rule and the records, and cleared once the rule holds again. The module's
health names it and stays healthy, because health says only whether the module works, and a finding never holds a
walk at the module's first-node gate (the operator's decision of 2026-10-12 on issue 504, as issue 508 decided for a proposal never
reviewed).
The rule `cited` is also judged ahead, in the review, before the judgement: a decision that names no home and that no
accepted record links is not accepted. Its review, by the knowledge base itself, asks changes naming the rule (issue
504). It is not refused at `propose`, because no record can link a number only reserved: a record that cites a new
decision is proposed after it and accepted, and the new decision is proposed again.
An accepted record is changed only by a new record, or by a progressive insight: a new version marked as one, which
is reviewed like a proposal.
@@ -148,6 +158,14 @@ per clause whether the record says exactly that; an accepted clause becomes `con
same way. When every record of a topic has been through both, the operator is told once per topic, with the counts
confirmed and still drafted. `return` with a record's number and a reason sends it back to be drafted again.
**A drafted clause may be changed, and a linked clause keeps its number** (ADR 0321, issue 494). A later record's
`changes` may name a drafted clause, confirmed or not, as it may one in force or changed; only a superseded or proposed
clause is refused. On that record's acceptance the drafted clause is marked `changed`, naming the clause that changed
it, keeps its number and stays `drafted`: it is still never served as in force, and `get` and `ask` say both. Because
a redraft numbers a record's clauses anew, `return` refuses a record any of whose clauses another record changed or
links by number, in a version served or proposed, naming each such link; a record that changes the drafted clause is
the way to correct it. A record no other record links by clause is returned as before.
## 8. The import
`import` takes a commit of this repository (and `dry-run`) and writes every decision record and research effort
@@ -184,10 +202,12 @@ repository keeps no copy of the records, generated or otherwise (issue 435, answ
|---|---|
| A proposal that breaks a per-record rule is refused, naming every reason | the module's table test, one case per rule of § 4 |
| A record is accepted only after the knowledge base's own judgement approves it, and, when it needs the operator, only on the warrant | the module's tests: a proposal whose judgement answers changes, or cannot be had, stays proposed; a `needs operator` record unaccepted without the warrant, accepted with it |
| A whole-set rule that fails is loud | the module's test: an acceptance that leaves a decision cited by nothing raises the condition |
| A decision is not accepted cited by nothing | the module's test: a decision with no home and no record linking it gets changes from the review, with no judgement asked; with a home, or once a record citing it is accepted, it is accepted |
| A whole-set rule that fails is loud, and leaves the module healthy | the module's test: a correction that leaves a decision cited by nothing tells the operator once, as a warning naming the rule and the record, not again after a restart, and clears it once a record cites it; the module's health stays healthy throughout |
| Every version is kept and comparable | the module's test: three versions, `history` lists them, the difference of the first and third is exact |
| The import changes nothing on a second run, and loses nothing | the module's test over a fixture repository; and the comparison of § 8 on issue 426 |
| A drafted clause is never served as in force | the module's test: `holds` over a record with only drafted clauses answers none, and `ask` marks them |
| A drafted clause may be changed, and a linked clause is never renumbered | the module's tests: a record changing a drafted clause is accepted, and the clause is then `changed`, keeps its number, stays drafted and is not served; `return` of a record whose clause another record changed or links by number is refused naming the link, and writes nothing |
| An answer stays within its bound | the module's test over the full import of `main` at a recorded commit: every `ask`, `get` and `holds` of a fixed question list |
| A record written in this repository meanwhile writes its clauses | `records.py`, check `clauses`, on every hq pull request, until the records left this repository (issue 426); since then `records.py`, check `no-records`, refuses any record written here |
| Live | the acceptance sentence, asked from a session outside this repository |