Design 47 milestone G: develop answers batch 5 (G-15 §560-§586), the operator's answers of 2026-11-11 afternoon #551

Merged
mesh-tools merged 3 commits from design/47-G-develop-answers-5 into main 2026-11-11 13:37:28 +00:00
7 changed files with 708 additions and 45 deletions
+1 -1
View File
@@ -978,7 +978,7 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283, 0564, 0668.
takes a restore point of a module's kept data on the live bus before the module moves across a `form` change, sets
the live data aside and restores the point at a revert, and lists and removes restore points; the bus module and
the snapshot account restore nothing into the live bus, and the backup holder only stores copies (design 47 G §554).
**aside** — live kept data a restore set aside, removed only by the operator's `discard-aside`.
**aside** — live kept data a restore set aside, removed only by the hand tool `recovery_discard_aside`, which the operator and sessions call.
*Not:* ~~restorer~~, ~~restore account~~
- **disk readings** — the size of each data item a node's modules declare, measured by its `measure` and kept
with its largest size in seven days by disk-load on that node, in its own state, one key
@@ -1340,7 +1340,8 @@ and `delivered` events.
- **A module may ship its own Dockerfile.** A build module of its own builds the module's `image` artifact from it, rootless, with no root and no container runtime socket on the build node; a pull request's image is built and thrown away, and only a merged trunk commit's image reaches the registry, by digest. From R4g that module is the only part that builds an image, and build-agent builds none (ADR 0487 §7, ADR 0524 §5); the other builds
are Go and Node programs, which their build modules build.
- **Every dependency comes by the version its lock file names**, checked against its checksum: the mesh's own
code from the mesh's forge, its own packages from its package registry, any other from its public source.
code from the mesh's forge, its own packages from its package registry, any other from its public source; a mesh
package's entry is the build's to write, by the version the planner read (ADR 0573 §2; design 47 G §571).
- **A build module fetches from the forge over SSH with a key of its own**, as its own account, under a forge
account for that module that reads the mesh's repositories and pushes nothing.
- **A build's reuse key** holds the installed toolchain's version and the build module's recipe version in place
@@ -88,7 +88,8 @@ check and deliver milestone G itself (ADR 0667 §2); each of them says so in a l
verb `branch` (its head, and whether a rule refuses a direct push and admits only merges), and refuse a watched branch
the forge does not protect, naming it (ADR 0666 §3).
- §6 **The foundation parts are the manifest validator's list.** The manifest validator's list (ADR 0619 §3) holds
exactly the foundation parts, ADR 0619 §1's and the runner, which joins them (ADR 0666 §4); the checker leaves the
exactly the foundation parts, ADR 0619 §1's, the runner, which joins them (ADR 0666 §4), and the forge and image
registry modules (§584); the checker leaves the
list when it is removed (*How milestone G itself is checked and delivered*). A registration holds no foundation
value. How it is checked: a unit test of the manifest validator refuses a template in the manifest of every part on
its list, the runner's included, and judges every other part's manifest as any part's.
@@ -366,9 +367,9 @@ check and deliver milestone G itself (ADR 0667 §2); each of them says so in a l
they are a chain (`mesh-grants:program`, then `node-tools:runtime`, then `mesh-grants:tools`).
- §48 **Removal runs the other way.** When something goes away the order is reversed: a seat moved to a new holder has its
old holder released before the new one claims (ADR 0666 §14).
- §49 **The edges alone order.** The build and the delivery order by these edges alone; a cycle of delivery edges (kinds 2,
4, 5, 6) is delivered as one step, and a cycle of build edges (kinds 1 and 3) is refused, naming every artifact on it
(ADR 0666 §14; research 086, R17). The cycle rule is the indexer's, in the catalog graph it keeps, so the checking
- §49 **The edges alone order.** (§569) The build and the delivery order by these edges alone; a cycle of delivery edges
(kinds 2, 4, 5, 6) alone is delivered as one step, and a cycle with any build edge (kind 1 or 3), whatever its other
edges, is refused, naming every artifact on it (ADR 0666 §14; research 086, R17). The cycle rule is the indexer's, in the catalog graph it keeps, so the checking
program's cycle rule goes with the checker.
- §50 **A format's reader first.** A format a running reader cannot read is preceded by its reader, an edge of kind 4 on
every watched branch (ADR 0666 §14): the indexer's manifest validator for a new field of a seat definition or a
@@ -5,6 +5,8 @@ design: 47
milestone: G
updated: 2026-11-11
decisions:
- "ADR 0500"
- "ADR 0573"
- "ADR 0672"
- "ADR 0667"
- "ADR 0666"
@@ -64,8 +66,8 @@ that round are written into the lines they name, here. Questions that an earlier
`node-backup` on its node as every item is (ADR 0404), which stores the copy and nothing else. When
`restore-kept` answers `missing`, the node agent calls it again with `from-backup: yes`: the recovery module asks
the seat `node-backup`'s `restore` of its item for that directory, which brings it back beside its path as that
verb always does, takes the point from there and restores it; when no repository snapshot holds it, `restored`
says `failed` with `restore point missing`, and §300 holds the revert.
verb always does, takes the point from there and restores it once its digest is checked (§567); when no
repository snapshot holds it, `restored` says `failed` with `restore point missing`, and §300 holds the revert.
- **The aside, and a restore that fails.** `restore-kept` sets the live kept data aside by taking a stream snapshot
of each of its bus objects into `aside/<module>/<kept>/<at>` in the same directory, reads the aside back to check it (`at` the UTC time written
`20261111T121200Z`, with `-2`, `-3` and on added when that name exists), only then removes the live objects, which needs no person's warrant, the data staying in the aside (the operator,
@@ -99,7 +101,7 @@ that round are written into the lines they name, here. Questions that an earlier
(recovery module), `kept-points` lists a point and an aside with their sizes, an aside is gone after
`recovery_discard_aside`, and a restart lists the same.
- §523 **A seat event whose subject has parts.** A seat definition's `subject` with `<part>` tokens (§397), such as
`mesh-recovery`'s `ended.<module>.<kept>` and `node-backup`'s `item-started.<node>.<module>` (§554), is said on
`mesh-recovery`'s `ended.<module>.<kept>` and `node-backup`'s `item-started.<node>.<module>.<item>` (§554), is said on
`mesh.seat.<seat>.event.` followed by the subject, each part filled in the definition's order: the seat
generator's subject function for the event takes each part as a string in that order and fills each as one token,
escaped as a key's name is (§73), refusing an empty part, `.` and `..`. A reader hears one module's events by the subject with
@@ -179,9 +181,13 @@ that round are written into the lines they name, here. Questions that an earlier
with no `parts` (from an assigner of before G, or for a node whose record does not state `want-parts`, §525) is
applied as today, its resources fetched by their sources and checked by their own digests. A want with `parts` is
applied only when the digest of every resource it fetched is among the values of the want's `parts`, and
every value of `parts` is the digest of a resource it fetched; otherwise it applies nothing of that module and its
have says the phase `parts-differ`, naming the digest. `parts` stays keyed by `<module>:<artifact>` for people and
the delivery record. This completes §379. Why: the digest is what is fetched and checked (develop question H-8). How
every value of `parts` is the digest of a resource it fetched; otherwise it applies nothing of that module, its
have says the phase `held` (one of the seat's phases, which gains none) with its detail opening `parts-differ:` and
naming each digest that differs, and it says `wait-held` with the reason `parts-differ` (§541). `parts`
stays keyed by `<module>:<artifact>` for people and
the delivery record. This completes §379. Why: the digest is what is fetched and checked (develop question H-8); a
hold is the phase `held` with its reason, as every hold of §300, §421 and §499 is, so the seat's phases stay as they
are (develop question of 2026-11-11 on H-8's phase, batch 5). How
it is checked: unit (node-engine), a want whose parts and fetched digests agree applies, and one with a fetched
digest outside `parts` or a part not fetched applies nothing, naming it; whole (node-engine of G), a want written by
an assigner of before G is applied.
@@ -355,9 +361,17 @@ that round are written into the lines they name, here. Questions that an earlier
- §528 **The checker built from G's heads asks no group build.** From G's delivery the pipeline is the check and the
run on request the try (§66), so the checker built from G's heads makes no group check: its group build through
tagged versions (its check's group path, `judge.go`'s, and `tagwanted.go`) goes with `tag-wanted` (§373), nothing replaces it in
the checker, and a group's builds against its package members' digests are the runner's (§373, §465). Until G's
the checker, and a group's builds against its package members' digests are the runner's (§373, §465). The
checker's old Node form goes with it: it no longer packs a group member's package for a Node fetch (ADR 0500 §5),
for any group, since a group's builds against its members' heads are the runner's (ADR 0573 §4). The checker's
manifest of G's heads consumes neither package registry's `published` and its asks stream carries no `tag-wanted`;
its `go.mod` requires nothing that no code of it imports; and the checker's seat definition in mesh-contracts
defines no `tag-wanted` holder event, which goes as §321 retires `tag-wanted`. Until G's
delivery the running checker makes G's own check with main's code (ADR 0667 §2). Why: §66 and §373 (develop
question CK10). How it is checked: the code review finds no group build in the checker of G's heads.
questions CK10 and the checker developer's three after it, batch 5). How it is checked: the code review finds no
group build and no Node packing in the checker of G's heads; unit (manifest validator), the checker's manifest of
G's heads accepted without the two `consumes`; unit (mesh-contracts), the checker's seat definition holds no
`tag-wanted`.
- §534 **The wait's `step` in the runner's body.** The runner's wait body keeps `step` as the pipeline step a wait inside
a pipeline is in (`lint`, `unit`, `integration`, `composition`, `review`, `approval`), and gains the value
`pipeline`, the acting step of §203, for a wait of the entries the runner holds for its new version (§266,
@@ -5,6 +5,7 @@ design: 47
milestone: G
updated: 2026-11-11
decisions:
- "ADR 0259"
- "ADR 0672"
- "ADR 0436"
- "ADR 0667"
@@ -51,25 +52,22 @@ are written into the lines of G-12 they name.*
- **Its seat, module and node.** The seat `mesh-recovery`, held once for the mesh, is defined in mesh-contracts
beside `mesh-broker`, and the module `mesh-recovery`, in mesh-catalog, claims it; its glossary word is the
*recovery module*. It runs on the node of the holder of `mesh-broker`, read from the catalog half and the seat
`mesh-module-assigner`'s placements, never a fixed name: G's seed writes its placement there, and the
placement verb refuses to move it while `kept-points` lists a point younger than 90 days, naming them, so its
points and their copies in that node's repository stay together. Its data directory, its manifest's `data` item
`points`, is capped at 10 GiB and measured by the disk readings (ADR 0571 §5): past 80 % it writes the finding
`points-filling`, and a `snapshot-kept` that would pass the cap ends `failed`, naming it, which holds the move as a
failed backup does (§296). Its `findings` state and finding stream are declared as §529's other parts declare
`mesh-module-assigner`'s placements, never a fixed name: G's seed writes its placement there. It moves as any
module that holds data, its points and asides holding no move (§562). Its data directory, its manifest's `data`
item `points`, is capped by its setting `points-cap` and measured by the disk readings (ADR 0571 §5): past its fill
mark it writes the finding `points-filling` (§561), and an act that would pass the cap ends `failed`, naming it,
before it removes anything; a `snapshot-kept` that ends so holds the move as a failed backup does (§296, §564). Its `findings` state and finding stream are declared as §529's other parts declare
them, so it waits on the bus module for them and is no foundation part (ADR 0619 §1).
- **Its verbs answer at once; the result is the event.** `snapshot-kept` and `restore-kept` answer `accepted` with
the act's `act` id, `busy` with the act that runs (it runs one act at a time and keeps no queue), or `error`, and
nothing more; the point comes on `ended`, the restore on `restored`. A caller told `busy` asks again when it hears
that act's event. The module writes each accepted act in its directory before it answers; at its start an act
the ending event of the act it was named (§560). The module writes each accepted act in its directory before it answers; at its start an act
accepted and not ended is ended `failed` with `stopped`, so its caller asks again.
- **Its events.** On `mesh.seat.mesh-recovery.event.` and kept last per subject, 90 days (§396): `ended.<module>.<kept>`
and `restored.<module>.<kept>`, each carrying `act`, `module`, `kept`, `form`, `fingerprint`, `snapshot` and
`outcome` (`done` or `failed`, with `error`), `ended` also the point whole; `restore-started.<module>.<kept>` when a
restore begins; `set-aside.<module>.<kept>` and `aside-discarded.<module>.<kept>` with the aside's `id` and
`size`; and `point-removed.<module>.<kept>` (§551). The node agent wakes only on the event whose `form` and
`fingerprint` are its ask's. Its wait for `ended` or `restored` is bounded at 90 minutes (§405), past which its have
says the wait and the watchman's `delivery-waits` raises it.
- **Its events.** On `mesh.seat.mesh-recovery.event.` and kept last per subject, 90 days (§396): `ended`,
`restore-started`, `restored`, `set-aside`, `aside-discarded` and `point-removed`, each with the parts
`<module>.<kept>`, and `expiry-ended`, their fields as §560 gives them. The node agent wakes only on the ending
event of its own act, and its wait for `ended` or `restored` is bounded at 90 minutes from its first ask (§405,
§560), past which its have says the wait and the watchman's `delivery-waits` raises it.
- **Who calls it.** `snapshot-kept` and `restore-kept` are called by a node-engine holder (`seat:node-engine`): the
recovery module takes a call only from an account the bus proves to be a node-engine's, and the identity of who
asked is the check (the operator, 13:06 CET), every act logged; no want is read for it, since the operator,
@@ -85,16 +83,24 @@ are written into the lines of G-12 they name.*
- **The operator's hand tools.** `recovery_take` (take a point of one module and kept data now), `recovery_restore`
(restore one point) and `recovery_retry` (ask again the last failed act of a module and kept data), each of class
`act`, callers `operator` and `sessions` (§391), written in the hand-act log; they are checked by identity, as above.
- **Its account.** Composed by the grants part from the seat's definition and the manifest: the bus's stream
§566 adds `recovery_restore_aside` and `recovery_cancel`.
- **Its account.** The operator, 2026-11-11 about 14:15 CET at lead-47's terminal (the knowledge base's review of
ADR 0672, R3): the recovery module is one process with a bus account of its own (ADR 0436 §1, as ADR 0626 §1 has
the bus module's keeper), which holds the seat `mesh-recovery` and answers its verbs itself; each verb is one its
caller names (`by-caller`, ADR 0259 §3), so the bus proves who calls: a node-engine's account for `snapshot-kept`
and `restore-kept`, and the tool runner's account, which the sessions and the operator reach the bus as, for its
hand acts. Its tools bundle, carried by the tool runner, calls those verbs and holds no grant of its own. ADR 0672
§4 records that this changes ADR 0436 §3 and §5 for this process alone. The account is composed by the grants part
from the seat's definition and the process's `bus` declaration: the bus's stream
snapshot and restore subjects with their chunk subjects, a stream's deletion and a durable consumer's creation,
only for the bus names of a module's kept data; the subjects its seat says and its hand-act log write; its
consumers of `node-backup`'s `item-started` and `item-ended`, each with the ordered
consumer §556 needs; its calls of `node-backup`'s `restore` as the caller `holder:mesh-recovery@same-node`, which
consumers of `node-backup`'s `item-started` and `item-ended`, filtered to those two events, each with the ordered
consumer §556 needs, from the process's `consumes`; its calls of `node-backup`'s `restore` as the caller `holder:mesh-recovery@same-node`, which
`node-backup`'s definition names for `restore` and which the backup holder takes only for the recovery module's
own item `points` (the operator, 13:02 CET); and nothing else. The bus names granted are those of every kept data
of every catalog graph entry the catalog half keeps (§443) and of every entry a restore point names (an old
section's name before a form change, a template's binding after), read by the grants part through
`kept-points`; no stream of the mesh's own (`CONTROL`, `NODES`, `EVENTS`, `DEAD_LETTER_NOTICES`, `DEAD_LETTERS`,
`kept-points` (§565); no stream of the mesh's own (`CONTROL`, `NODES`, `EVENTS`, `DEAD_LETTER_NOTICES`, `DEAD_LETTERS`,
`PROV_APPLIED`, `CHECKS_READY`, `CHECKS_OTHER`, every `SEAT_` stream and every `KV_` bucket of a seat) is ever
granted. Restricted streams (ADR 0436) are among the names, as ADR 0672 records.
- **Bus names.** The recovery module turns a module and a kept data into bus names from the catalog graph entry of
@@ -105,19 +111,20 @@ are written into the lines of G-12 they name.*
it, so the server has at most two snapshots at once. No `restore-kept` starts while the dump runs, and no dump of
the bus starts while a restore runs: the holder of `node-backup` of G says `item-started.<node>.<module>.<item>`
and `item-ended.<node>.<module>.<item>` for every item, kept last per subject, 7 days, and puts off the bus's item
while a `restore-started` of the recovery module has no `restored`; the recovery module learns the bus module and
while a `restore-started` of the recovery module has no `restored`, the dump giving way when both start at once
(§563); the recovery module learns the bus module and
its node from the seat list's definition of `mesh-broker` and the catalog half's claim of it, never a fixed name.
Both read these streams as §556 has it, so a restart mid-dump or mid-restore misses neither. A blocked act waits,
neither refused nor failing a want: past 60 minutes the recovery module writes `recovery-waits` and the backup
holder `backup-waits`, each naming the item or the act it waits on, and `kept-points` lists the act that runs
with since when and what it waits on. The recovery module consumes the item events that G's backup holder adds,
an edge of kind 6 from that holder to it, so a backup holder of before G, which says none, is never relied on.
- **Its health.** Its declared health check calls `kept-points`, and fails while any aside's record ends at
`removed` (§516), when it also writes the finding `aside-unrestored` naming the module, the kept data and the
aside's `id`.
- **Its health.** Its declared health check calls `kept-points`, and fails only while an aside's record ends at
`removed` (§516) and no running act holds it, so a restore under way never fails it; it then also writes the
finding `aside-unrestored` (§561), which `recovery_restore_aside` (§566) clears.
- **Its order.** The node-engine's `invokes` of its verbs gives the edge of kind 6 from the recovery module to the
node-engine, so §298's first edge reads "the new version of the recovery module" (§555). At a revert of G the
edge runs backward (ADR 0656 §4): the node-engine goes back only after every module it restores there said
edge runs backward, as the order does when something goes away (ADR 0666 §14): the node-engine goes back only after every module it restores there said
`restored` and runs (§298), and the recovery module, new in G, goes only after the node-engine, so it stays while
any restore runs; its data item stays on its node, with its copies.
- **Its tools** are named `recovery_`.
@@ -172,7 +179,7 @@ are written into the lines of G-12 they name.*
- §558 **A field of a served verb's answer is an edge of kind 6.** Kind 6 (§374) orders a module after a version that
serves a verb or a word it uses, and a verb's presence alone says nothing of a field its answer gained, so a
consumer relying on a new field of a served answer could run before the provider's new version and fall back in
silence (the grants part's first view relies on `binding` in `mesh-broker`'s `list`). An `invokes` entry therefore
silence (as the grants part's first view relied on `binding` in `mesh-broker`'s `list` before design 25's §9). An `invokes` entry therefore
takes a long form, `{verb, fields}`: `verb` as today (`seat:<seat>.<verb>`), `fields` the names of its answer's
fields the consumer relies on. The manifest validator refuses a field the seat's definition does not give that
verb's answer, naming it. The delivery plan derives the edge of kind 6 from the holders of that seat to the
@@ -184,14 +191,15 @@ are written into the lines of G-12 they name.*
form, and the running checker, the indexer and the grants part, which read manifests through it, are rebuilt with
it by the release's publish (kind 1), as for `max-mib`. That publish rebuilds every module importing mesh-sdk's
`catalogue`, the set the release rebuilds already, so the form adds no rebuild. The consumers of G that name fields:
the grants part, `mesh-broker`'s `list` with `binding`; every reader of `mesh-catalog`'s `revision` through
every reader of `mesh-catalog`'s `revision` through
mesh-sdk's client (the bus module, the status view, the condition keeper, the tool runner, the assigner, the grants
part and the runner), with `none-yet` (§530); the status view, `mesh-catalog`'s `entry` with `manifest-digest`
(§506). The recovery module reads the manifest whole the entry carries (§443), a field of before G. Any other part of G relying on a field G adds to a served answer names it so in G's change, which the
(§506). The grants part names none: design 25 *bindings-first-view* §9 composes a binding from the fields `list` answered before G, so it relies on no
new field of `list`, and naming `binding` would close the circle of §569. The recovery module reads the manifest whole the entry carries (§443), a field of before G. Any other part of G relying on a field G adds to a served answer names it so in G's change, which the
code review checks. Why: the module graph orders a consumer after what it relies on, and a field is part of the contract
(lead-47's question from the operator's "the module graph is for this", 2026-11-11). How it is checked: unit
(manifest validator), a field the definition lacks refused; unit (delivery plan), the grants part ordered after
a bus module whose definition gives `binding`, and not before; unit (the release's reader), both forms read.
(manifest validator), a field the definition lacks refused; unit (delivery plan), a consumer naming a field
ordered after a holder whose definition gives it, and not before; unit (the release's reader), both forms read.
- §559 **A bundle's finding writer publishes and hears through two runtime calls.** A bundle, in Go or TypeScript,
holds no bus connection, so mesh-sdk's finding writer (§461, §503) in a bundle uses the runtime channel's existing
calls, each gaining one argument: `mesh/publish` takes `ack: true`, and the tool runner then publishes the event on
@@ -206,4 +214,5 @@ are written into the lines of G-12 they name.*
languages over the one channel a bundle has, as §518 (develop question of 13:08 CET on §461). How it is checked:
unit (tool runner), a publish with `ack` answered with its sequence and a refused one with `{error}`, a `deliver:
new` subscription given no message older than it; whole (mesh-sdk, TypeScript and Go bundles), a finding written
and an ask answered through a tool runner of G.
and an ask answered through a tool runner of G; unit (mesh-sdk, both languages), the writer fails at once, naming
`finding-calls` and the node, against a tool runner that does not state it.
@@ -0,0 +1,635 @@
---
layer: to-be
status: in-progress
design: 47
milestone: G
updated: 2026-11-11
decisions:
- "ADR 0672"
- "ADR 0668"
- "ADR 0667"
- "ADR 0666"
- "ADR 0664"
- "ADR 0626"
- "ADR 0619"
- "ADR 0596"
- "ADR 0595"
- "ADR 0573"
- "ADR 0564"
- "ADR 0539"
- "ADR 0500"
- "ADR 0429"
- "ADR 0404"
- "ADR 0283"
- "ADR 0259"
- "ADR 0235"
---
# 47, milestone G: develop's fifth batch, and round 3's notes on §516 to §559
One file of milestone G's part of [design 47](../47-delivery-from-commit-to-delivered.md); the part opens at
[G.md](G.md). Its design lines are numbered across every file of the part and cited as "design 47 G §n".
*§560 to §586 answer develop's fifth batch (lead-47's input of 2026-11-11), the reviewers' round-3 notes on §516 to
§559, the knowledge base's review of ADR 0672 (the recovery module), and round 1 of this batch's own review, written
into the lines it names. Seven carry the operator's answers of 2026-11-11 at lead-47's terminal, each line saying
so: §562 (the module's move), §563 (a put-off dump), §568 (the builder's retry), §570 (refused asks), §575 (a failed
build's words), §584 (the forge and image registry in the foundation) and §554's account. The rest are of internal
form only. A note that corrects a line of G-12 or G-14 is written in that line; here only what is new.*
## The recovery module
- §560 **Every event of the recovery module says its act, and its caller wakes on its own act.** (§554, §516, §551)
Every event of the seat `mesh-recovery` carries `act`, the id `snapshot-kept` or `restore-kept` answered with
`accepted`, or a hand tool's own act id (§566), and `at`, the UTC time it was said in RFC 3339. Each event's other
fields:
- `ended.<module>.<kept>`: `module`, `kept`, `form`, `fingerprint`, `snapshot`, `outcome` (`done` or `failed`),
`error` when failed, and on `done` the point whole (§516).
- `restore-started.<module>.<kept>`: `module`, `kept`, `form`, `fingerprint` and `snapshot`, said before the
restore reads the dump's item events and before any aside is taken (§563).
- `restored.<module>.<kept>`: `module`, `kept`, `form`, `fingerprint`, `snapshot`, `outcome`, `error`, `aside` and
`missing` (§516).
- `set-aside.<module>.<kept>` and `aside-discarded.<module>.<kept>`: the aside's `id`, `name` and `size`.
- `point-removed.<module>.<kept>` as §551 has it; and `expiry-ended`, a subject without parts, with `removed`, the
number of points the run removed, 0 included.
- **Every act ends with an event under its `act`.** `snapshot-kept` and `recovery_take` end with `ended`;
`restore-kept`, `recovery_restore` and `recovery_restore_aside` (§566) with `restored`; `recovery_aside` with
`set-aside`; `recovery_discard_aside` with `aside-discarded`; a run of §551's removal (at the start, once a day,
at a `snapshot-kept` or by `recovery_expire_points`) with `expiry-ended`, also when it removes nothing;
`recovery_retry` with the ending event of the act it asks again, under an `act` of its own; `recovery_cancel`
(§566) with the cancelled act's ending event, `outcome` `failed` and `error` `cancelled`.
- **An act a restart cut off** (accepted and written in the directory, never ended, §554) is ended at the module's
start, after the start's repair of asides (§516, §564), with its own ending event under its `act`, `outcome`
`failed` and `error` `stopped`.
- **A restore runs** from its `restore-started` until a `restored` with the same `act`; the backup holder reads it
so (§554, §563).
- **The caller wakes on its own act.** The node agent told `accepted` wakes only on an `ended` or a `restored`
whose `act` is the one it was given; a `form` and a `fingerprint` alone wake nothing, since a retried act's old
`failed` carries the same ones. Told `busy` with the running act's `act`, it wakes on that act's ending event
and asks again, and its new answer gives the `act` it waits on next.
- **The 90-minute bound** of the node agent's wait (§554) counts from its first ask for that point or restore,
every `busy` and every ask again within it. Past it, the node agent's have says the wait and the watchman's
`delivery-waits` raises it.
- **How the node agent hears and keeps it.** The node-engine's manifest `consumes` `mesh-recovery`'s `ended` and
`restored`, read as §556 has it. Before it asks, the node agent writes in its have the want's wait with `ask-at`
(its first ask's time); once answered `accepted`, it writes the `act` there too. After a restart it reads that
have: an `act` whose ending event the read of §556 already gives is taken as heard, and one not yet ended is
waited on, asked nothing again; a wait with no `act` yet is asked again within its bound.
- **What the node agent does with each ending.**
| outcome, `error` | what the node agent does |
|---|---|
| `done` | goes on with the move or the revert |
| `failed`, `stopped` (a restart or a move cut the act off, §562) | asks again, within the same bound |
| `failed`, `cancelled` (§566) | asks nothing again; holds the move, the reason `cancelled` with the hand act's `why` |
| `failed`, `cap` (§564) | holds the move, the reason the error's own words |
| `failed`, `restore point missing` or `restore point damaged` (§516, §567) | holds the revert as §300 has it, the reason the error's own words |
| `failed`, any other error | holds the move, the reason the error's own words |
A hold is the phase `held` with that reason, said as `wait-held` (§541), and ends on a new want or the
operator's hand act (`recovery_retry`, then a new ask of the node agent on that act's ending).
This replaces §554's list of events. Why: an event a caller cannot tie to its own call wakes it on another's
failure, and an act whose end is never said holds its caller unseen (round 3, health and events, items 1 and 2;
running and testing, item 3). How it is checked: unit (mesh-recovery's contract), each event's fields read; whole
(recovery module), a retried act's caller not woken by the first act's `failed`, an expiry run that removes nothing
says `expiry-ended` with `removed` 0, an act cut off by a restart ends `failed` with `stopped`; unit (node agent),
`busy`, then the other act's ending event, then an ask again within one bound, the bound passed after repeated
`busy` answers, and each row of the table; whole (node-engine, recovery module), a node-engine restarted during a
restore takes that restore's `restored` and asks nothing again.
- §561 **The recovery module's and the backup holder's findings, each with its scope, id and clearing.** (§554) Each
is written by mesh-sdk's finding writer (§461) as the key `<scope>.<id>.<kind>` (ADR 0564 §2), each token escaped
as a key's name is (§73), and said on the part's finding stream (§383, §529):
- `points-filling`: the recovery module's, scope `mesh-recovery`, id the node it runs on. It is written when the
directory of its data item `points` passes the fill mark of its cap (§564), which the recovery module measures
itself at its start, after each act and after each expiry run, naming the size, the cap and the five largest points and asides. It is cleared when a
measure finds it below the mark.
- `recovery-waits`: the recovery module's, scope the module whose act waits, id the act's `act`. It is written
60 minutes after the act began waiting on the dump of the bus's item (§563), naming the item and since when. It
is cleared when the act starts its work, or ends.
- `backup-waits`: the backup holder's, scope its node, id the item that waits (`<module>.<item>`, one escaped
token). It is written 60 minutes (its setting `put-off-bound`, §557) after the item was put off (§563), naming the restore's `act`. It is cleared
when the item starts again.
- `aside-unrestored`: the recovery module's, scope the module, id the aside's `id`. It is written at an act's end
and at the module's start for each aside whose record ends at `removed` and that no running act holds, which
the health check also fails on (§554). It is cleared when that
aside's record ends at `restored` or the aside is discarded.
The recovery module and the backup holder of G are observers on the condition keeper's list of sources (§387,
§388), written into it in G's change. Why: a finding no one can clear stays open for ever, and the four findings
of §554 named no scope, id or clearing (round 3, health and events, item 4). How it is checked: whole (recovery
module, backup holder, the keeper), one whole test writes and then clears each: the points directory filled past
the mark, then an aside discarded; an act held by a dump past the bound (a setting, §557), then started; a dump
held past it, then started; an aside left at `removed` with no running act, then restored by
`recovery_restore_aside`.
- §562 **The recovery module moves as any module that holds data; its points and asides hold no move.** (§554) The
operator, 2026-11-11 about 14:15 CET at lead-47's terminal: the recovery module may be moved off its node at any
time, and the restore points and asides in its directory do not hold the move. A placement that moves it is made as
for any module that holds data. The assigner asks for the move a person must allow before it writes the want (ADR
0539 §14). The old node's want is written as removed, and its data item stays on that node's disk, with its copies
in that node's repository, until the operator answers (ADR 0429 §6). On its new node the recovery module starts
with an empty directory: `kept-points` lists nothing of the old node's, and a `restore-kept` of a point left there
answers `missing`. Its `from-backup: yes` then asks the new node's backup holder, which holds no copy of it, so
`restored` says `failed` with `restore point missing` and §300 holds the revert. The placement verb asks the
recovery module nothing, and §554 no longer refuses a move while a point younger than 90 days is listed.
- **An act a move cuts off.** When its node's want is written as removed, the old instance takes no new call
(answering `error` naming the move) and, before it stops, ends its running act through its failure path (§516):
a restore past its removal puts the aside back under the live names and sets the consumers back; then it ends
every accepted act, the running one included, with its ending event, `outcome` `failed` and `error` `stopped`.
- **A stop it could not finish** (the old node gone or killed): the new instance, at its start, reads the seat's
stream as §556 has it, and ends `failed` with `stopped` every act whose `restore-started` or acceptance (its
`ended` or `restored` missing) the stream holds with no ending event, and writes `aside-unrestored` for each such
restore, naming the old node, since its aside is on the old node's disk.
- **Who asks again.** The node agent and the backup holder hear those endings (§560, §563); the node agent asks
again the new holder, which it finds from the catalog half's claims of `mesh-recovery`, and the backup holder
starts a put-off item again on a `stopped` `restored`.
Why: the operator's answer (the knowledge base's review of ADR 0672, R6); the ending of a cut-off act is the
design's (round 1 of this batch's review, running and testing R2, health and events H1). How it is checked: unit
(assigner), a placement that moves the holder of `mesh-recovery` while points are listed is made, asked as ADR
0539 §14 has it; whole (recovery module), on an empty directory a restore of an unknown point says `restore point
missing`; a move during a restore after its removal leaves the live data as before and ends the act `stopped`; a
new instance whose old one was killed during a restore ends that act `stopped` and writes `aside-unrestored`; a
put-off dump starts again after it.
- §563 **When a dump and a restore start at one moment, the dump gives way.** (§554) Each side says its start before
it reads the other's:
- **The backup holder** of G, for the item of the holder of `mesh-broker` (read from the seat list's definition
and the catalog half's claim, §554), says `item-started`, then reads `mesh-recovery`'s `restore-started` and
`restored`, last per subject, once more. When a restore runs (§560), it ends the item at once with `item-ended`,
`outcome` `put-off` and `restore` naming that act, having read nothing of the bus, and starts the item again when
it hears `restored` with that `act`.
- **The recovery module** says `restore-started`, then reads the item events of the bus's item. While an
`item-started` has no `item-ended`, it waits, and takes no aside.
- **Said means acknowledged.** Each side reads the other's only once the bus acknowledged its own start event on
its stream.
Since each side says its start before it reads, at least one side sees the other, and the backup holder always
gives way, so the two never overlap. A dump that has run since before the restore began is waited for. The backup
holder ends every item of its own that it said started and never ended, at its start, after its read of §556, with
`item-ended`, `outcome` `failed` and `error` `stopped`, so no restore waits on a dump that no longer runs. Every
`item-started` and `item-ended` carries `at`, and `item-ended` its `outcome` (`done`, `failed` or `put-off`).
- **A put-off dump is that night's backup.** The operator, 2026-11-11 about 14:05 CET at lead-47's terminal: an
item ended `put-off` is neither a failed night nor a missed one (ADR 0235 §13, §21 hold): the item runs when
`restored` is heard, and the night is the outcome of that run. The item's last good backup ages as before, so
`backup-stale` is raised only by its own age rule, never for the put-off itself. `backed-up` (§302) lists a
put-off item with the restore's `act` and since when; `backup-waits` names it after 60 minutes (§561).
Why: §554 left open which side gives way when both start at once (round 3, running and testing, item 1); how a
put-off item counts is the operator's answer (the knowledge base's review of ADR 0672, R5). How it is checked:
unit (backup holder and recovery module against a fake bus), both orders of "say, then read" in both sides, each
ending with no overlap; whole (backup holder, recovery module, the real bus), a dump and a restore asked at one
moment never overlap and the item runs after `restored`; a put-off item raises no `backup-stale` and is not counted failed; a backup holder
restarted during an item ends it `failed` with `stopped`, and a waiting restore then starts.
- §564 **The cap is a setting, an act that would pass it is refused before it removes anything, and a failed act
leaves no half-made point or useless aside.** (§554, §557)
- **The settings.** The cap of the data item `points` is the recovery module's setting `points-cap`, 10 GiB unless
set, and its fill mark the setting `points-filling-mark`, 80 % unless set, both read at its start, as §557 has a
bound.
- **Before it writes.** Before it writes anything, `snapshot-kept`, `recovery_take` and the aside of
`restore-kept` add up the bytes the bus states for each bus object they take. An act that would pass the cap
ends `failed` with `error` `cap`, naming the cap, that size and the directory's fill, before any aside is taken
and so before any live object is removed. A `snapshot-kept` that ends so holds the move as a failed backup does
(§296). A point or an aside that passes the cap while it is taken (the stream grew) ends its act the same way.
- **Brought back.** A point brought back from the backup (`from-backup: yes`, §516) is not counted against the
cap: it lies beside its path only while its restore runs, and is removed when the restore ends.
- **On a failure.** An act that ends `failed` removes its point under a temporary name (§516) then, not only at
the next start. An aside whose record ends at `set aside` (the live objects were never removed) is removed when
its restore fails or stops before the removal, and at the start, since the live data was never touched.
Why: the cap is a bound a whole test sets (§557), and a restore refused for size after the removal would leave the
module's data only in an aside (round 3, running and testing, item 2 and its notes). How it is checked: whole
(recovery module, with small settings), a snapshot passing the cap ends `failed` and holds the move; a restore whose
aside would pass the cap is refused with the live data untouched; `points-filling` written past the mark (§561); a
failed point leaves no temporary name; an aside left at `set aside` by a stopped restore is removed at the start.
- §565 **The grants part reads the restore points' names through `kept-points`, and composes without them until it
answers.** (§554) The bus names of the recovery module's account include those a restore point names (§554). The
grants part reads them by its `invokes` of `seat:mesh-recovery.kept-points`, a kept call (ADR 0668 §1). It asks
again on every `ended` with `outcome` `done`, every `point-removed` and every `expiry-ended` of `mesh-recovery`,
which it consumes (ADR 0668 §4), so a point's names are granted before any restore of it. Until `kept-points`
first answers after a start, the grants part keeps the names it last granted from it, kept in its state
`memberships` with the account, and adds the catalog half's kept entries (§443); it takes no name away before
that answer, and says the wait in `grants_first_views` (§556). The edges between the two (kind 5, the grants part has acted for the recovery module's account; kind 6,
the recovery module serves `kept-points`) are a cycle of delivery edges, delivered as one step (§49). Why: the
account must follow the points without the grants part reading another part's state (ADR 0596; round 3, seams and
tools' notes; running and testing R3). How it is checked: unit (grants part), a restarted grants part with
`kept-points` unanswered keeps a point's names it granted before, and a point's old-section names are added after
an `ended` and taken away after its `point-removed`.
- §566 **Two more hand tools: put an aside back, and cancel a waiting act.** (§554, §516) Each is of class `act`,
with callers `operator` and `sessions`, checked by the identity of who asked (§554), written first in the hand-act
log of the seat `mesh-hand-acts` with the caller the bus proved and `why`, and an act of its own that runs one at a
time (ADR 0672):
- `recovery_restore_aside` (`module`, `kept`, `id`, `why`) restores an aside under the live names it was taken from,
for an aside whose record ends at `removed` that no running act holds (the case `aside-unrestored` names, §561).
It refuses, naming them, while live objects of those names exist (an aside taken by `recovery_aside` is put
back by this tool too, the refusal naming `recovery_aside`'s aside when the names are live), and it ends with
`restored`.
- `recovery_cancel` (`act`, `why`) ends an accepted act that has not begun its work (one waiting on the dump, §563)
with its ending event, `outcome` `failed` and `error` `cancelled`. It refuses an act that has begun, naming its
step, since a restore cut off after its removal is put back only by its own failure path (§516).
Why: a module's tools recover what a person may need to recover (*Tools are how a module is spoken to*; round 3,
seams and tools' notes). How it is checked: whole (recovery module), an aside left at `removed` put back by
`recovery_restore_aside` and refused while the live names exist; a waiting act cancelled and a running one refused.
- §567 **A restore point is checked by the digest of its bytes before it is restored.** (§516) A point's record
carries `digest`, the `sha256:` digest of each bus object's snapshot file, written when the point is renamed whole
(§516). The same digests are kept on the bus, apart from the bytes: in the recovery module's own state `digests`
(its kept data, by the template `state`), one key per point, its `snapshot` escaped as a key's name is (§73),
written before `ended` is said and deleted when the point is removed (§551); about 0.3 KiB a key, capped at 4 MiB.
`kept-points` lists the digests, and `ended` carries them with the point. Before a restore takes any aside, it
computes the digest of the bytes it is about to restore, whether from its own directory or brought back by the
backup holder (`from-backup: yes`), and compares it with the key in `digests`; the record in the directory is never
the reference. The state holds digests only and lists nothing, so §517's "no `bus-points` state" stands for the
list. A mismatch ends the act `failed` with `restore point
damaged`, naming the point and the bus object, and §300 holds the revert. Why: a point brought back from the backup
was restored unchecked (round 3, security and upgrade's notes). How it is checked: whole (recovery module, backup
holder), a point whose file and record were both altered after it was taken is refused before any aside is taken.
## The builder
- §568 **A transport fault in the builder's own read ends no build key, and a key's error says which kind of fault it
is.** (ADR 0664 §1 to §3, ADR 0668 §1 to §3; issue 1612, met again 2026-11-11)
- **The builder's own reads.** The builder's reads of the build seats' state `steps` (its own step keys, §467,
§481) are kept calls (ADR 0668 §1, §2). A read that meets a transport fault, of ADR 0664 §1's classes or the
read's own deadline, changes nothing the builder holds and writes no build key. The builder's watch of `steps`
takes up again from its last revision once the bus answers, and a read it made at its start is made again then.
Past five minutes it writes `call-unanswered` (ADR 0668 §3). A build key is written `failed` only from a step
key's end (ADR 0664 §3).
- **The words of a failed key.** A step that ended failed because its third run met a transport fault (ADR 0664
§2) gives the build key an `error` that opens `transport fault:`, then the class (`no responders`, `name lookup`,
`connection` or `deadline`), the step's seat and node, and the cause of its last run in the build module's words.
Every other failure keeps its own opening (`not stored:`, `not pinned:`, `version taken:`, `refused kind:` or the
build's own words) and never opens `transport fault:`. The builder's read tools show the error whole, so a reader
tells a passing cause from a real one without the journal.
- **What builds such a key again.** The operator, 2026-11-11 about 14:15 CET at lead-47's terminal, decided by the
record of the builder's retry (number to come), which changes ADR 0664 §3: a key whose error opens `transport
fault:` is built again, once, as a new revision of the key (ADR 0429 §18), on the first event after its failure
that says its cause passed. Two events say so: the node-engine seat's `running.<node>.<module>` saying `running`
for a module whose catalog graph entry claims the build seat the failed step ran on (§546), on any node; and the
node-engine's `manifest-reads-changed` of the node the failed step ran on (ADR 0668 §4), which it says when that
node is heard again. The builder hears both through its consumers of the node-engine seat, read as §556 has it.
It builds a key again at most once per event heard, and only while the key's newest revision is that failure, so
one event starts one revision. A revision that fails again on a transport fault is said as every failed key is,
its error opening `transport fault:` again, and waits for the next such event. The builder's manifest `consumes`
both events.
- **Three automatic builds, then the operator is asked.** The operator, 2026-11-11 about 14:55 CET at lead-47's
terminal: the builder counts, in the key's newest revision, the revisions events started since the last one a
person started (`event-builds`). After three that failed, the next such event starts nothing: the builder asks
the operator once, through the seat `operator-channel`'s ask (level `approve`, options *Build once more* and
*Leave it*, `about` the key so a newer ask replaces it, ADR 0666 §12), naming the module, the fingerprint and
the last error whole. *Build once more* builds one revision, counted as a person's, so the count starts again;
*Leave it*, a decline or the ask's expiry leaves the key failed until `builder_build_again` or a new
fingerprint. The failure stays said throughout. A key failed for any other cause
is never built again on these events. `builder_build_again` (ADR 0668 §5) and the builder's next start build it
again too, as before. The builder's read tool of a key names the event that built its newest revision again,
`event-builds`, and the open ask.
- **Before develop.** These lines rest on the record of the builder's retry, which changes ADR 0664 §3 (in force);
it is accepted before G's builder is built from them, and its number replaces "number to come" here.
Why: a read the builder made met `no responders` and failed a key, which then stayed failed for about 45 minutes
(issue 1612, 2026-11-11), against ADR 0664 §3 (a key is not written failed while its step runs on); the retry on an
event is the operator's answer. How it is checked: unit (builder), a `no responders` read of `steps` leaves the key
`building` and the next revision ends it; a third-run transport fault's key error opens `transport fault: no
responders`, and a compile error's does not; a `running` of a holder of that build seat builds the key again once,
and a second `running` while the new revision builds starts nothing; a node's `manifest-reads-changed` builds again
a key failed on that node; a key failed on a compile error is not built again on either event; after three failed
event-started revisions the fourth event asks the operator once and builds nothing, and *Build once more* builds
one.
## The order of a change
- §569 **A cycle with a build edge is refused, whatever its other edges, and a grant composed from a first view is an
edge.** (§49, §558, §374) The indexer's cycle rule (§49) reads every edge of the six kinds in one graph per watched
branch. A cycle whose edges are all delivery edges (kinds 2, 4, 5 and 6) is delivered as one step. A cycle with at
least one build edge (kind 1 or 3) is refused, whatever its other edges, at the check and at take-in, naming every
artifact on it in order with each edge's kind and the manifest word it came from: no build on such a cycle can
start before a delivery that waits for that build (ADR 0666 §14). Two kinds of edge enter that graph:
- **A seat verb a build calls.** When a build module's step calls a seat verb while it builds (the image build
module calling `container-image-registry`'s `mirror` for a base image), its manifest names it in `invokes`, and
the indexer derives an edge of kind 3 from that seat's holders to every image artifact the build module builds.
The edge is met by the holder that runs when the build starts, whatever its version: it orders a build after a
running holder, never after the holder's new version. So the holder's own artifacts are built through its
running version, and the edge makes no cycle through them.
- **A grant from a first view.** A binding the grants part writes for a holder from its first view of a served
answer (design 25 *bindings-first-view* §4 and §9)
is an edge of kind 5 from the grants part to that holder. Where the grants part names a field of that answer
(§558), an edge of kind 6 runs from the answer's seat holder to the grants part. Its new version must run, so this
edge is met only by that version.
The circle of 2026-11-11 was such a cycle: the bus module's image waits, by kind 3, for the registry holder's
delivery; the registry holder waits, by kind 5, for its binding `mirrors` from the grants part; and the grants part
waited, by kind 6 on `mesh-broker`'s `list` with `binding`, for the bus module's new version: the kind-3 edge waited
for a registry that served `mirror`, which needed its binding, which needed the new bus module. It is refused at its
check, naming the three artifacts. Design 25 *bindings-first-view* §9 removes the kind-6 edge: the grants part
composes a binding from the fields `list` already answers, so it relies on no new field and names none (§558).
G's delivery then puts the grants part before the bus module's build. Why: the circle mixed a build edge and a
delivery edge, and no rule saw it (issue 1966, notes 203 and 204; the operator, 2026-11-11 13:30 CET, on design
25's §9). How it is checked: unit (indexer's graph), a cycle of kinds 3, 5 and 6 is refused naming its three
artifacts and their kinds; a cycle of kinds 5 and 6 alone is one step; a build module's `invokes` of `mirror` gives
a kind-3 edge to its image artifacts; the image registry's own image, built through its running holder, is not
refused.
## Asks to the operator
- §570 **An ask the router refuses is heard by its asker, and no asker asks for longer than its level allows.**
(ADR 0259 §3, §4) The kind behind it is an asker left waiting on an ask the router never took (issue 1610; met
again 2026-11-11, when the bus module's three `nats_recreate` asks were refused at take-in and their tool kept
saying the operator had not answered).
- **The router says the refusal.** The router (the holder of `operator-channel`) says every ask it refuses at
take-in, for each cause ADR 0259 §4 gives as it holds, on that asker's
`mesh.seat.operator-channel.event.decided.<asker>`. The event carries `outcome` `refused`, the ask's `id`, `why`
in the router's words and `at`, as an ask's end without a warrant is said (ADR 0259 §3).
- **Every asker ends its request on it.** mesh-sdk's asker client gives its caller `refused` with `why` as soon as
it hears it. An asker's tool or act that waits on that ask answers `refused` with why, never "not answered yet";
the bus module's `nats_recreate` and `nats_drop` are among them.
- **The lifetime is held where every ask is made.** mesh-sdk's asker client changes in G's mesh-sdk release, which
every asker is built against (§47). mesh-sdk's asker client, through which every asker publishes
an ask, refuses before it publishes an ask whose expiry passes the bound of its highest option level (`approve`
24 hours, `destroy` 10 minutes, ADR 0259 §4), naming the level and the bound. The bus module's keeper asks for
its level's bound in place of its fixed 12 hours (`AskLasts`).
- **The operator sees a refused ask.** The operator, 2026-11-11 about 14:15 CET at lead-47's terminal: the router
keeps each ask it refuses at take-in in its bucket `asks` under `<asker>.<id>` as a closed record, with `outcome`
`refused`, `why` in the router's words and `at`, for 30 days like every closed record (ADR 0259 §3). It writes
the record only by a create: when `<asker>.<id>` holds a record already (an open ask, its tickets with it), the
refusal writes nothing there and is said on `decided` alone. The operator, 2026-11-11 about 15:15 CET ("just make
it configurable"): the router keeps at most `refused-kept` refused records per asker, a setting of the router,
20 unless set, removing the oldest first, each also gone after its 30 days, each removal written in its journal. The seat's
`asks` lists it with that outcome and words, as an ask that ended.
Why: ADR 0259 §4 refuses such an ask on the decided subject and bounds a destroy option at 10 minutes, and neither
reached the asker; the list is the operator's answer. How it is checked: unit (mesh-sdk's asker client), a table
over the levels in which an expiry at the bound is published and one past it is refused before it is published,
and a `refused` that ends the request with why; the code review refuses an ask built outside the client; whole
(router, bus module), a refused ask is said on `decided`, a refusal of an id that is open leaves the open record and its tickets as they
were, a twenty-first refusal of one asker removes its oldest, `asks` lists it `refused` with the router's words, and
`nats_recreate` answers `refused` with those words.
## A module's packages
- §571 **A mesh package's entry in a lock file is written by the build, never by hand.** (ADR 0573 §2, §4; §47,
§478, §504) A module's `package.json` names a mesh package (one whose name §504 maps to a catalog artifact) with the
range `*`, or with the oldest version it accepts (ADR 0573 §2). Its committed `package-lock.json` may hold any entry
for that package, or none.
- **What the build writes.** The Node and TypeScript build module removes every entry of a mesh package from the
workspace's lock before it installs. It then writes the entry from the version the planner read and the
integrity the package registry gives for that version (ADR 0573 §2). In a delivery group's check where a member
publishes the package, it writes the entry from that member's head, packed for the check, with the packed
tarball's integrity (ADR 0573 §4, ADR 0500 §4). Every other entry is installed as the lock names it and checked
against its integrity (*Every dependency comes by the version its lock file names*).
- **Before its first publication.** A package of which no version is published yet (`npm-package-registry`'s
`versions` answers none, as for the packages G itself publishes first) is reached only in a group's check, from
its member's head. Outside a group, the edge of kind 1 (§47) builds the dependent after the package's first
publication, which G's delivery orders.
- **The check's lock rule** never refuses a mesh package's entry, nor its absence.
- **The reuse key** of a build takes the versions it wrote for mesh packages, as the fingerprint takes them (ADR
0573 §2), so a build is never reused across two of them.
Why: ADR 0573 §2 has the build write a mesh package's version into the workspace, and a lock made by hand holds
the integrity of a locally packed tarball, which the published one never matches (develop question of the forge
module's developer, 2026-11-11). How it is checked: unit (Node build module), a lock naming a local tarball's
integrity for a mesh package is built with the registry's version and integrity; a lock without the entry is built
alike; a non-mesh entry with a wrong integrity is refused; whole (G's group check), the forge module's tools are
built against the head of the member that publishes the package, packed.
## The builder's forms
- §572 **The builder's process answers a package's name, and its tools ask it.** (§478, §504) The mapping from a
package's name to its module and artifact is the builder process's own, from its view of the catalog half (§348).
The seat `mesh-builds`' `reserved`, given `name`, is answered by the builder's process from that view. The
builder's tools bundle holds no catalog view: `builder_versions`, asked by a name, calls `reserved` (or, for a
module, `reserved` with `module`) through its `invokes` and answers what the process answers. Why: one mapping
and one view, in the part that keeps them (ADR 0596 §2; develop question BL7's name part). How it is checked: unit
(builder), `reserved` with an npm name and with a Go module path answered from the view; whole (builder),
`builder_versions` by name answers the same as `reserved`.
- §573 **The build seat's ask carries `version`.** (§370, §458) The ask of each build seat (`node-build-go`,
`node-build-node` and the image build seat) gains the field `version`: the version `MAJOR.MINOR.PATCH` the builder
reserved for the build (§369), or empty for a build that reserves none (§371, §373). The field is part of the build
seats' contract package, beside the ask's other fields. The builder fills it when it asks, and each build module
reads it and passes it as `MESH_VERSION` (§370), passing none when it is empty, so the build stamps none (§458). A
build module of before G ignores the field and stamps nothing. Why: §370 named no field (develop question of the
builder's developer on CT5). How it is checked: unit (build seats' contract), `version` read and an empty one
read as none; whole (Go and TypeScript build modules), a build asked with a version carries it.
- §574 **`toolchain-agreed` carries `digests` as a list.** (§472, §495) The event's field `digests` is a list, each
item `{digest, nodes}`: the `sha256:` digest of the toolchain program and the nodes that reported it at the agreed
version, sorted by digest, with each item's `nodes` sorted. It replaces the field `digest`, which the event no
longer carries; a kept event of before G that carries `digest` alone is read as
one item with no `nodes`. Why: §472 names one digest per group of nodes, and a list keeps two digests of one version apart
(develop question of the contracts developer). How it is checked: unit (mesh-builds' contract), the list read, and
an event of before G carrying `digest` alone read as one item with no `nodes`, so a reader of G refuses none
and nothing waits for the next agreement.
## What a node-engine says of a build
- §575 **A want held for its build says how the build stands.** (§524) The operator, 2026-11-11 about 14:05 CET at
lead-47's terminal: the words "the build of <module> at <fingerprint> is not done" are the assigner's, the reason
of its hold under its first rule (no want for a fingerprint whose build is not done, ADR 0429 §6). That reason now
says how the build stands, from the builder's `build.ended` the assigner already consumes and from the build it
waits on: a failed build reads `the build of <module> at <fingerprint> failed: <the error>`, the error whole as
`build.ended` carries it, `transport fault:` included (§568); a build that runs reads `the build of <module> at
<fingerprint> is being built`. The assigner says it in its wait for that module, which the watchman's
`delivery-waits` names; the node-engine reads no build state for it (ADR 0596). "Is not done" is said no more.
Why: the operator's answer; a failed build said as not done left the operator and the watchers waiting for a
build that had ended (round 1 of this batch's review, seams and tools T1, placed the words in the assigner). How
it is checked: unit (assigner), a module whose build ended failed is held saying `failed:` with the error, and one
whose build runs saying `is being built`.
## The tool runner, and kept data at run time
- §576 **A bundle reads its tool runner's capability words through one call.** (§488, §545, §559) The tool runner
answers the runtime call `mesh/capabilities` with `{words}`, the capability words it states (§545). mesh-sdk's
bundle, in Go and in TypeScript, makes that call once, at its first call that needs a word, and keeps the answer
for as long as it runs. A bundle lives no longer than the tool runner that started it (its channel is that tool
runner's standard input and output), so after a restart of the tool runner the bundle is started again and reads
the words again. A tool runner of before G answers `mesh/capabilities` as an unknown call, which the bundle reads as
no word stated, so a call of a family G adds fails at once, naming its word and the node (§488). A bundle learns
its node from `MESH_NODE`, which the tool runner sets in the environment of every bundle it starts, read alike in Go
and in TypeScript. Why: §488 named no channel (develop question of the mesh-sdk developer). How it is checked: unit
(tool runner), `mesh/capabilities` answers the stated words; unit (mesh-sdk, both languages), the words read once,
an unknown-call answer read as none, and the node read from `MESH_NODE`.
- §577 **A part reaches the kept data its `requires` declares through its binding, never by an old name.** (§529;
ADR 0595 §1, §9; ADR 0619 §2)
- **A bundle** reaches it only through the tool runner's `mesh/` calls by its local name (ADR 0595 §9), which the
tool runner resolves in the module's membership binding.
- **A process with an account of its own** reaches it by its binding: mesh-sdk's process package gains `Binding`,
which takes a local name and gives the entry's bus names (bucket, stream and subjects, object stores). It asks the
seat `mesh-broker`'s `show` for the module and the local name at the process's start, as a kept call (ADR 0668 §1)
that the process's `bus` names in `invokes`. It asks again when it hears `mesh-broker`'s `recreated` or `dropped`
for that entry, which the process consumes. Until `show` answers, the process acts on nothing that needs the entry
and its health says it waits, naming the entry (ADR 0283).
- **The old name** (mesh-sdk's opening of an essential bucket by its declared name) is for a foundation part's old
`state` section alone (ADR 0619 §2). A part that declares an entry in `requires` opens it by `Binding`; the forge
bridge's `performed` is among them, moved so in G's change.
- **The validator's rule for the finding words** (§529) accepts the old stream `<module>-findings` only for a part
on the foundation list (ADR 0619 §3 as ADR 0666 §4 changes it). It refuses that stream for every other part, whose
finding stream it requires in the template form: `finding-events`, by the template `events`, its entry listing
`finding-written.*`, `finding-cleared.*` and `findings-stated.*`.
Why: a part's kept data is the bus module's to name and the part's to ask for (ADR 0595 §1, §7), and §529 left the
run-time path and the validator's row open (develop question of the watchman's developer). How it is checked: unit
(mesh-sdk's process package), `Binding` answered from `show`, asked again on `recreated`, and waiting while
unanswered; unit (forge bridge), `performed` opened by its binding; unit (manifest validator), a foundation part's
old finding stream accepted, another part's refused and its template form accepted.
## The indexer's forms
- §578 **`manifest-digest` carries its prefix, and the key does not.** (§506, §445) The catalog graph entry's field
`manifest-digest` is `sha256:` followed by the 64 hex digits, as every digest a record names. The module's value
and §445's `manifests` key stay the 64 hex digits as on main, since a key's name takes no `:` (§73), and mesh-sdk's
catalog client gives the bare digits as today. The indexer writes all three from the one digest it computed at take-in.
Why: one digest in two forms, the field's as the records write a digest and the key's as a key's name allows
(develop question of the indexer's developer). How it is checked: unit (indexer), the entry's field after its
prefix equals the key's name and the module's value.
- §579 **The indexer keeps the records pinning each revision, and removes a revision only on an event.** (§153,
§178, §216)
- **Where.** The indexer, a foundation part, keeps its old `state` section `pins`: one key per catalog graph
revision, `<branch>.<revision>` (the watched branch and the revision's id, each escaped as a key's name is, §73),
whose value is the list of delivery record ids that pin it. A list of ids is kept rather than a count, so a
`pinned` heard twice pins once. At about 20 revisions pinned a day for 90 days, about 1,800 keys of about 0.2 KiB
each, it is capped at 4 MiB (an estimate).
- **Applying `pinned` and `freed`.** The indexer adds or removes a record's id by compare-and-set, and only then
acknowledges the event on its durable consumer of `mesh-delivery`'s events (§153). That consumer, when first
made, starts at the stream's first message. A `freed` said while it was stopped is applied when it comes back,
and one applied twice changes nothing. A `pinned` of a revision the indexer no longer holds writes its key with
the record's id and `missing: true`, and the finding `pin-missing` (scope the indexer, id the revision) naming
the record, which `indexer_unpin` clears.
- **The gate states its pins.** At each start the gate says `pinned` again for every revision each record it keeps
pins, then once `pins-stated` (`at`), so pins said before the indexer's consumer existed, or older than the
stream keeps, are heard again; the set form absorbs the repeats.
- **Current.** A revision is current while it is the newest revision the indexer took in on its watched branch,
whichever watched branch that is, or the newest revision of an open candidate (its `candidates.<group>`). It stops
being current when a newer revision of its branch is taken in, or when the candidate's `heads-replaced` is heard
(its heads replaced, or its pull request closed) or the candidate is taken in.
- **Removal.** When a revision's list is empty and it is not current, the indexer removes its revision object and
its key, and its entry objects as §179 has it, but only while its consumer of `mesh-delivery`'s events is caught
up (none of its messages pending), so a `pinned` said before is applied first, and only once it has applied a
`pins-stated` said after its state `pins` was made. It judges on applying `freed`, on a revision ceasing to be
current, on being caught up, and at its start after both of these hold, when it judges every kept revision, so a
stop between a take-in and a removal keeps nothing for ever. Never on a timer.
- **Inputs.** A catalog graph revision names no input revision. "The input revisions it names" (§178) are an
assignment graph revision's catalog revisions, which a delivery record pins itself (§153), so the indexer keeps
each by its own list alone.
- **The assigner** keeps its pins of assignment graph revisions in the same form and by the same rules: its old
`state` section `pins` (it is a foundation part, ADR 0619 §1), one key per assignment graph revision, capped at
4 MiB; a revision is current while it is the newest the assigner wrote, or the newest it wrote for an open
candidate; its tools are `assigner_pins` and `assigner_unpin`, as the indexer's below.
- **Its tools.** `indexer_pins` reads each kept revision with the records that pin it, whether it is current and
why, and the store's fill against its cap. `indexer_consumer` reads each of the indexer's durable consumers: its
stream, name, acknowledged sequence, the messages pending, and the last event applied with its `at`. Both are
of class `read`. `indexer_unpin` (`revision`, `record`, `why`; class `act`, callers `operator` and `sessions`,
written in the hand-act log first) removes one record's id from a revision's list, for a `freed` that was lost,
and the removal rule then applies.
Why: §153 and §178 said what is kept, not where or how it is counted (develop questions IX17 and IX18). How it is
checked: unit (indexer), a revision pinned twice by one record and freed once is removed; a `freed` applied twice
changes nothing; a revision of a second watched branch kept while current; a candidate's revision removed on
`heads-replaced`; a revision that stops being current while a `pinned` of it is pending is kept; a `pinned` of a
removed revision writes `pin-missing`; `indexer_unpin` frees a revision; whole (indexer, gate), a `freed` said
while the indexer was stopped is applied after its start; a `pinned` said before the indexer of G first started is
heard through the gate's restatement; an indexer stopped between a take-in and a removal removes the revision at
its start.
## Observers with more than one bundle, and the forge module's forms
- §580 **Each finding kind belongs to one bundle, and a statement covers only its own kinds.** (§383, §384, §485)
A module whose bundles write one state `findings` (the forge module has four: its TypeScript tools, its Go package
registry, its forge and its agent token) gives each finding kind to exactly one bundle. That bundle's mesh-sdk
writer is made with the kinds it owns, and refuses a `write` or a `clear` of any other kind, naming it. Each bundle
judges and states its own kinds from its own truth (§384); no bundle reads the whole state to state another's
findings. Its `findings-stated` carries `kinds`, the kinds it states, and the writer's statement deletes only keys of
those kinds. The keeper sets its view of that observer, node and set of kinds from the statement, and clears only
conditions of those kinds that the statement does not name. A statement with no `kinds` (a writer of a part with
one bundle) covers every kind, as before. Why: a statement that deletes every key it does not state would clear
another bundle's findings (develop question of the forge module's developer, GIT14). How it is checked: unit
(writer, both languages), a statement with `kinds` deletes no key of another kind, and a write of another kind is
refused; whole (keeper), two bundles' statements on one node each keep the other's conditions open.
- §581 **A `seen` key's 30 days are judged on each step end, with no timer.** (§487) The forge module writes `at` in
each `seen` key's value, the time of the `step-ended` that wrote it. On each `step-ended` it handles, and at its
start, it deletes the `seen` keys whose `at` is more than 30 days old, at most 100 for each event, the oldest first,
so the bus keeps no key time-to-live and no template option is added; its tool `git_seen_expire` (class `act`,
callers `operator` and `sessions`, written in the hand-act log) runs the same removal now. The forge module being a foundation part
(§584), its state `failed-steps` is in its old `state` section, and a bundle reaches it through the tool runner's
state call by its name, as every old section is reached. Why: §487's 30 days needed
a trigger, and an event the module already handles is one (develop question of the forge module's developer). How
it is checked: unit (forge module), a `seen` key older than 30 days is deleted at the next `step-ended`, and a
younger one is kept.
- §582 **`reserved` by name answers a released version from the catalog.** (§381, §478, §369) Given `name` and
`version` with no reservation of that version (§369 deletes a reservation once its version is released), the
builder finds the module from the name (§572) and asks the seat `mesh-catalog`'s `versions` for that module, as a
kept call in its `invokes`. When the catalog holds that version released, `reserved` answers `released` with the
artifacts and the digests the release recorded, and the registry holder answers its publish as held when the
digest is the release's, storing nothing (§381). A version neither reserved nor released, or a name no entry
carries, is answered `error`, naming which, and the holder refuses the publish. Why: the holder knows only the name,
and the module is the builder's own mapping (develop question of the forge module's developer). How it is checked:
unit (builder), `reserved` for a released version answers `released` with its digests, and an unknown version
`error`; whole (registry holder, builder), a publish of a released version at its digest stores nothing.
- §583 **A manifest's first `version` is §368's.** The `version` G writes into the forge module's and the image
registry module's manifests is the `MAJOR.MINOR` of the version §372 fills for each, read through
`builder_versions`, and `0.1` for a module with none published (§368); their old values (`"1"` and `"2"`) carry
nothing over. Why: §368 answers it (develop questions GIT15 and MAN1). How it is checked: as §368.
- §584 **The forge module and the image registry module are foundation parts.** (§6, §529, §569; ADR 0619 §1 to §3)
The operator, 2026-11-11 about 14:30 CET at lead-47's terminal: ADR 0619 §1 is followed as written. The bus
module's build calls both before the bus module can answer, the forge for its source and the image registry's
`mirror` for its base image, so both are foundation parts.
- **The list.** The manifest validator's list (§6, ADR 0619 §3) gains the holder of the seat `git` (the forge
module, `gitea`) and the holder of `container-image-registry` (the image registry module, `distribution`) in G's
change.
- **Their kept data.** Each keeps its kept data in its old sections (ADR 0619 §2): its state `findings` and its
finding stream `<module>-findings` as §529 has a foundation part declare them, and every other state in its old
`state` section, the forge module's `failed-steps` among them (§581). The image registry module's `holds`,
`windows` and `mirrors` go back from their template entries to its old `state` section under the names they had
there. The operator, 2026-11-11 about 15:15 CET ("whatever must be done to not end up in a cycle"): at its first
start, before it serves anything, the image registry of G copies every key of its template `holds`, `windows` and
`mirrors` into its old sections, from its own data alone, and says in its journal and its read tool how many keys
it copied of each; a key already in the old section stays. This needs no other part's answer and adds no edge,
so it can close no cycle; holding its delivery until the template state is empty could wait for ever, since the
running registry keeps writing it. The template entries, no longer declared, then become orphans that `orphans`
lists, removed only by the operator's `drop` (ADR 0595 §4, §5), as §529 has it for the healers' old stream.
- **The delivery order.** The running image registry, of its build before G, serves `mirror` only once its bindings
are composed, and both the bus module's new build and the image registry's own new build ask it for their base
images. So the grants part with design 25 *bindings-first-view* §9 is delivered first, alone, and composes the
running registry's three bindings from `list`'s fields. Then
the image registry's and the bus module's builds of G are built through the running registry's `mirror` (edges of
kind 3, §569), and delivered by the edges of §49. Once the image registry of G runs, its state is in the old
sections, which the grants part creates as it creates every foundation part's, and no binding of a first view is
needed for it.
Why: the operator's answer; the list and the order follow ADR 0619 §1 and §3 and §569. How it is checked: unit
(manifest validator), both manifests of G's heads accepted with their old sections, and a template entry in either
refused; the catalog's validation at G's heads refuses neither; whole (image registry), a first start with keys in
its template `mirrors` copies them, says how many, and then serves them; the delivery plan of G puts the grants part's
delivery before both builds.
- §585 **What the forge module reads from `step-ended`, and the form of its `failed-steps`.** (§39, §475, §487, §581)
The forge module's readings where no line spoke, made design lines:
- **The fingerprint** of a step is the body's `fingerprint`, the build-input fingerprint, which the runner fills
in every `step-ended`. No field of the step's `result` is read for it. A `step-ended` without it writes the
forge module's finding `step-unread` (scope the module the step is of, id the run), naming the run, the module and
the step, cleared by a later `step-ended` of that step with its fingerprint, so a failure that could not be
recorded is seen.
- **A failure** is a `step-ended` whose `outcome` is `failed` and whose `status` is `required`. An `optional` or
`bypass` step's failure, and a `could not run`, record no failure, as ADR 0666 §11 refuses only a failed
`required` step.
- **A rerun's pass** clears a failure when the body says `rerun: true` and its `by`, the caller the runtime proved
as the runner's verb `rerun` names its callers, is `operator` or `seat:mesh-healers`; any other `by` clears
nothing (§39).
- **The watched branches** are those of each registration whose git origin is a repository the forge holds, per
repository, read through `mesh-catalog`'s `registrations` and kept current on `registration-changed`.
- **A `seen` key** `seen.<commit>.<module>` holds `{v, commit, module, fingerprints, at}`: `v` the form's version
(1), `fingerprints` every build-input fingerprint the runs named for that commit and module, and `at` as §581
has it. Both key forms of `failed-steps` are the forge module's own, defined in its own package, since no other
part reads that state.
- **The fill** of `failed-steps` that its read tool shows is counted by the forge module from the keys and value
bytes it reads, against the state's cap of 64 MiB, and said as counted, since a bundle's state call gives no
fill of the bus's own.
Why: the forge module's developer read these where no line spoke (develop, batch 5); each follows §39, §475, §487
and ADR 0666 §11. How it is checked: unit (forge module), a `required` failure recorded and an `optional` one not;
a rerun pass by `operator` clears and one by another caller does not; a `seen` value read in its form, and both
key forms; a `step-ended` without `fingerprint` writes `step-unread`.
- §586 **The client of `reserved` is the seat `mesh-builds`' contract package.** (§381, §478, §582) The forge
module's package registry and the image registry module both ask `reserved`. The call is the seat `mesh-builds`'
contract, so its client lives where ADR 0573 §1 puts a seat's contract: the package the seat's holder's
repository publishes, mesh-delivery's contracts package, which defines `mesh-builds` (§52). That package gains a
`mesh-builds` client with `reserved`, its input and its answers (`reserved`, `released`, `error`), and both
registry holders use it in place of their own copies. It is not put in mesh-sdk, which keeps no seat-specific
verb (ADR 0573 §1). Why: code two modules share has one home, and for a seat's verb that home is the seat's
contract package (develop question of the forge module's developer). How it is checked: unit (mesh-delivery's
contracts), the client's input and each answer read; the code review finds no second copy in either holder.
+5 -2
View File
@@ -5,6 +5,7 @@ design: 47
milestone: G
updated: 2026-11-11
decisions:
- "ADR 0500"
- "ADR 0672"
- "ADR 0436"
- "ADR 0670"
@@ -64,9 +65,10 @@ decisions:
- "ADR 0391"
- "ADR 0363"
- "ADR 0289"
- "ADR 0283"
- "ADR 0253"
- "ADR 0240"
- "ADR 0283"
- "ADR 0259"
- "ADR 0235"
- "ADR 0233"
---
@@ -79,7 +81,7 @@ file on 2026-11-10, and is cited as "design 47 G §n". A sentence that holds a d
stayed in the main file, and G builds what it says from G's delivery. A section named in italics is in this part, or,
when it is not here, in the main file.
The part is fourteen files, each read whole, its design lines numbered across them:
The part is fifteen files, each read whole, its design lines numbered across them:
- [G-1-pipelines-registration-and-steps](G-1-pipelines-registration-and-steps.md): pipelines per module: registration, watched branches, steps and policy, what holds a delivery, approval, groups, dependencies, a publish.
- [G-2-pipelines-sizes-checks-and-tools](G-2-pipelines-sizes-checks-and-tools.md): pipelines per module: any size, the pipeline makes the check, who may say what, health, recovery, tools, kept data, how G itself is delivered, the choices under ADR 0666 and ADR 0667.
@@ -95,3 +97,4 @@ The part is fourteen files, each read whole, its design lines numbered across th
- [G-12-develop-answers-fourth](G-12-develop-answers-fourth.md): the operator's answers of 2026-11-11 noon, round 3's leftovers and develop's fourth batch of questions (§516 to §553).
- [G-13-review-round-3](G-13-review-round-3.md): the review of §480 to §515, rounds 2 and 3 (§600 on).
- [G-14-recovery-module-and-review](G-14-recovery-module-and-review.md): the recovery module, the operator's answer of 2026-11-11 12:55 CET, and the review of §516 to §553 (§554 to §559).
- [G-15-develop-answers-fifth](G-15-develop-answers-fifth.md): develop's fifth batch, round 3's notes on §516 to §559, the knowledge base's review of the recovery module's record, and the operator's answers of 2026-11-11 about 14:05, 14:15 and 14:30 CET (§560 to §586).