From 2d8947fa04723a5ce902147ec079d1bfd0eb0a4a Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 13:14:19 +0100 Subject: [PATCH] Take in round 2 of design 47's G review: running kept per subject, the merge waits on the Approve alone, ADR 0661 named Round 2 found the pass's running events could age out of a new consumer's reach, the merge bullet still holding for a carry, the checker's own owed acts unanswered, findings that never cleared, and kept-artifact open to any module and caller. Each is now written as the lead ruled, with its test row. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- .../47-delivery-from-commit-to-delivered.md | 119 +++++++++++------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md index 07f1605e..42318e0d 100644 --- a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md +++ b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md @@ -4,6 +4,7 @@ status: in-progress code: [mesh-controller, mesh-catalog modules/mesh-delivery-gate, mesh-catalog modules/gitea, mesh-delivery, mesh-host, mesh-sdk, mesh-vault, mesh-contracts, hq 00-META/checks] updated: 2026-11-09 decisions: + - "ADR 0661" - "ADR 0660" - "ADR 0659" - "ADR 0656" @@ -542,7 +543,9 @@ checker's `mesh/check` from a watch of the checker's state `owed`: reads of anot performed` (the forge did not answer within the call's bound, with the error) or `undecodable` (an act it cannot read, with why): an act it cannot decode is said, never only logged. The gate hears the stream `forge` through its durable consumer, writes the outcome into the delivery record, and says an act's id is answered only once an - outcome is recorded. + outcome is recorded. The checker does the same for its own acts: it hears `owed.ended` for its own ids, records each + outcome in its own state (it watches only its own state), says again at its start, under the same id, every act of + its own still unanswered, and lists one unanswered past 15 minutes in its own stalled read. - **What is unanswered is said again, and stalls by a bound.** At its start the gate says again, under the same id, every act its records hold unanswered. An act unanswered 15 minutes after it was said is listed by `stalled`, which the condition keeper raises as the stalled line (ADR 0421 §7, ADR 0564 §2): fifteen minutes is many times a @@ -778,8 +781,11 @@ Findings 11). How each is kept, read and retained is this design's choice under - **Who holds a seat is applied by one rule, from what each node runs.** The node-engine and the grants part decide a seat's pass by mesh-sdk's `SeatsToPassOn` (ADR 0660 §3), reading the manifest of the build each assignment runs (ADR 0660 §2). What each node runs is heard, never asked: each part keeps it from the node-engine seat's events - `running.` (*What each step says*), through a durable consumer of its own declared in its `consumes`, so a - pass is decided with no call to a node that may be away. The event carries the module, the fingerprint and the + `running..` (*What each step says*), through a durable consumer of its own declared in its `consumes`, + so a pass is decided with no call to a node that may be away. The seat's stream keeps the last message per subject + with no age limit, so the last `running` of every module on every node stays on it however long ago it was said, + and each part's durable consumer starts from the last message per subject, never from now: a consumer made after a + node's last `running`, and two parts' consumers made at different times, hold the same builds. The event carries the module, the fingerprint and the digest of the build's manifest, which is the indexer's manifest of that build (ADR 0660 §2's "the indexer's manifest of it"), read by digest from the catalog half; the builder's done key is not needed, since the event names the digest it would give. A node that has said no `running` for a module since the part's consumer began counts by @@ -944,7 +950,7 @@ say another runs a build (ADR 0363 §2); the gate's events are granted to the ga | an entry built | `build.ended` | the builder's own | `mesh.stream.mesh-delivery-builder.event.build.ended` | the builder's manifest | | an entry checked | `checked` | `mesh-delivery-checker`, the checker | `mesh.seat.mesh-delivery-checker.event.checked` | the seat's definition in `mesh-delivery-contracts` | | an entry wanted | `wanted` | `mesh-wants`, the assigner | `mesh.seat.mesh-wants.event.wanted` | the seat's definition in `mesh-delivery-contracts` (ADR 0641 §2) | -| an entry composed, and running healthy | `composed`, `running` | `node-engine`, the node-engine on each node | `mesh.seat.node-engine.event.composed.`, `.running.` | the seat's definition in `mesh-contracts` | +| an entry composed, and running healthy | `composed`, `running` | `node-engine`, the node-engine on each node | `mesh.seat.node-engine.event.composed.`, `.running..`, the latter on a stream kept last per subject with no age limit | the seat's definition in `mesh-contracts` | | a node's capabilities changed | `capabilities-changed` | `node-engine`, the node-engine on that node | `mesh.seat.node-engine.event.capabilities-changed.` | the same | | a wait held, and ended | `wait-held` (step, acting part, its version, the entry, since when, its bound), `wait-ended` (the same, and how: ran, released) | each holder: `mesh-wants` for the assigner (placing, wanting and composing), `mesh-grants` for the grants part, the builder's own stream | `mesh.seat..event.wait-held`, `.wait-ended`; `mesh.stream.mesh-delivery-builder.event.wait-held`, `.wait-ended` | each seat's definition; the builder's manifest | | a store filling, full, and back below its cap | `filling`, `full`, `room` | `mesh-catalog`, `mesh-wants`, `mesh-delivery` | `mesh.seat..event.filling`, `.full`, `.room` | each seat's definition | @@ -1064,7 +1070,7 @@ names go, since every new acting part needed code changes elsewhere (research 08 event says. - **Composing waits per node, by the assigner.** A node's wants wait only for that node's own node-engine, never for another node's, so a node away holds only itself (ADR 0603 §7). The want gains no field: the assigner, which writes - the wants, holds a node's other wants until the node-engine seat's `running.` says the new node-engine version + the wants, holds a node's other wants until the node-engine seat's `running..` says the new node-engine version runs on that node, and writes that node-engine's own want first. A node-engine of before milestone G thus gets its own want first and every other want once it runs the new one, so an old node-engine never composes a module it cannot read. The clock of that node's bound starts when the node is present and its node-engine's want is written, @@ -1087,16 +1093,27 @@ names go, since every new acting part needed code changes elsewhere (research 08 never waited for: the running build module builds the shared contracts and the registry holder, then the new build module builds the rest, so ADR 0614 §6's cycle through them stays allowed, now for that reason. - **The artifact store's holder takes its own new build even when its seat is the one broken.** A build module - stores what it builds through the seat `oci-artifact-registry` (or `container-image-registry` for an image), so the + stores the artifacts it builds through the seat `oci-artifact-registry`, so the holder of that seat is in its own build's path: its new build needs the store it replaces. As every holder's own inputs are (above), that store is acted on by the running holder: the running holder stores its new build, and only then is the new one wanted. When the running holder is what is broken (the seat refuses or does not answer a store, as in the hand bootstrap of 2026-11-07 to 11-09), the build module keeps that one build on its own node among its kept builds (*The bootstrap delivery*: `build_go_kept`), says in its step key that it is kept on that node, and the - builder's done key names it so; the node-engine composing the holder fetches it by its digest through the build - seat's verb `kept-artifact` of that node, and the new holder, once it runs, takes the store of every build after - it. This is the one artifact the mesh delivers from a build node instead of the store, and only for a holder of a - store seat; the design's choice under ADR 0656 §3 (a wait skips what the holder's own new version needs) and ADR + builder's done key names it so with its digest; the node-engine composing the holder fetches it through the build + seat's verb `kept-artifact` of that node, and the new holder, once it runs, takes the store of every build after it. + - The verb sits in the build seats' definitions (`node-build-go`, `node-build-node`, `node-build-image`, in + `mesh-delivery-contracts`), and its callers are the node-engines alone: the definition names them, and the grants + part grants it to no other account. + - It answers only for a module that claims the seat `oci-artifact-registry`, and refuses every other, naming the + module, so a store outage never turns into delivering from build nodes. + - The bytes go as an object by digest on the bus (the old `objects` form, ADR 0619 §2), never in a value, since an + artifact is over the bus's value limit; the node-engine checks the bytes it fetched against the digest in the + builder's done key and applies nothing that differs. + - While the keeping node is away, or its sweep removed the build, the want waits, and the node-engine says in its + have that it waits for that node or for a build no longer kept. + + This is the one artifact the mesh delivers from a build node instead of the store, and only for the holder of + `oci-artifact-registry`; the design's choice under ADR 0656 §3 (a wait skips what the holder's own new version needs) and ADR 0526 §2, whose build by a session's call it makes a step of the delivery instead of a hand act. How it is checked: test 60. - **The check refuses a cycle among the waits within one step** (ADR 0656, which changes ADR 0603 §2's refusal of a @@ -1191,12 +1208,13 @@ commit on the SDK's trunk or the head of an SDK pull request the forge holds, op again when the nodes change, which judges heads no check judged. From research 085's milestone its verdict says `could not run`, naming the part not heard, and the check releases its claim on the verdict key, so the heads can be judged again. The part's return is the one event that can change the outcome: the node-engine seat's - `running.` naming that part (a node waking, the forge module or a build module starting again alike), on which + `running..` naming that part (a node waking, the forge module or a build module starting again alike), on which the check is asked again, once per set of heads and per part returning, never on a timer; a `running` of the same part at the same fingerprint after that asks nothing. A `could not run` verdict not asked again within 60 minutes becomes the checker's finding `check-could-not-run`, naming the heads and the part, which the condition keeper - raises (ADR 0564 §2): an hour is the check's own bound. A second check of the same heads asked while one runs, or - one erred, claims nothing and writes no status, by the claim above (issue 1373; ADR 0548 §1, ADR 0656 §5). How it + raises (ADR 0564 §2): an hour is the check's own bound. The finding clears when the heads are checked again or + replaced, or their pull request closes. A second check of the same heads asked while one runs, or after one that + erred without releasing its claim, claims nothing and writes no status, by the claim above (issue 1373; ADR 0548 §1, ADR 0656 §5). How it is checked: test 58. - **A failed build node loses nothing.** An ask a build module took and never acknowledged goes to another node by the stream's own acknowledgement wait, with no timer of a part; a step's end is written on the build module's step key named @@ -1259,8 +1277,8 @@ the check gates merges.* ### Nothing merges into a repository under check -*ADR 0548 §2 to §4, §9, §10, as ADR 0551 bounds them; from research 085's milestone removed, by the operator's word, -its record still to be written (below).* +*ADR 0548 §2 to §4, §9, §10, as ADR 0551 bounds them; from research 085's milestone removed by ADR 0661 (proposed, +needs the operator's Approve; below).* **Removed at research 085's milestone.** The hold, the merge turns, their stalled line, the gate's tools for them and the setting `check-gates-merge` go in milestone G, the gate's and the forge module's alike (the operator, on Telegram, @@ -1269,9 +1287,9 @@ operator's word for G is that no check gates delivery (1921's note of 2026-11-07 is handed to the forge bridge at once; a check running on a repository the merge goes into ends on the commit it read, and its verdict is carried (*A move of the trunk carries every verdict*); the checker reads nothing of the gate, and the gate no longer watches the checker's verdicts for a hold. **Its record is to be accepted before the push:** ADR -0548 §2 to §4, §9 and §10, ADR 0551 and ADR 0534 §4's judgement of `mesh/check` by the merge are in force, and the -record that ends the first two and changes the third (needs the operator's Approve) is to be accepted before milestone -G's pull requests are pushed; until it is accepted, the rule below holds as those records have it. How it is checked: in the gate's whole-module test an approved merge into a +0548 §2 to §4, §9 and §10, ADR 0551 and ADR 0534 §4's judgement of `mesh/check` by the merge are in force, and ADR +0661, which ends the first two and changes the third (proposed, needs the operator's Approve), is to be accepted before +milestone G's pull requests are pushed; until it is accepted, the rule below holds as those records have it. How it is checked: in the gate's whole-module test an approved merge into a repository under a running check is handed at once, and the gate's manifest declares no `turns` and no setting `check-gates-merge`; in the checker's, a check claims at once with no gate state granted to read. @@ -1575,11 +1593,13 @@ pull request, and it owes the forge nothing. - **The merge.** mesh-delivery merges through the forge's merge tool at the heads the Approve names, only while they are still the heads, their check passed against the trunk's head, and, while the check gates merges, no check of other heads runs on a repository the merge goes into (*Nothing merges into a repository under check*, - above, which milestone G removes). From milestone G the forge bridge merges through the seat `git`'s verb - `merge-pull`, which judges the Approve's heads and no check (*The seat `git`*, under *The verbs*). When the trunk moved, the merge waits for the carried verdict, and the event of its line asks the merge - again; no timer asks it, no person rechecks, and the Approve is not asked again (ADR 0421 §3, §12, as ADR 0548 - §8 changes them). The merge tool still refuses a verdict naming a trunk commit other than the trunk's head and - asks nothing (ADR 0534 §4); the carry makes the verdict current. + above, which milestone G removes). Until milestone G, when the trunk moved, the merge waits for the carried + verdict, and the event of its line asks the merge again; no timer asks it, no person rechecks, and the Approve is + not asked again (ADR 0421 §3, §12, as ADR 0548 §8 changes them); and the merge tool refuses a verdict naming a trunk + commit other than the trunk's head and asks nothing (ADR 0534 §4), the carry making the verdict current. From + milestone G the merge waits on nothing but the Approve's heads and a trunk that takes no push: the forge bridge + merges through the seat `git`'s verb `merge-pull`, which judges those two and no check, and a trunk move holds no + merge for its carry (ADR 0661, to be accepted before the push; *The seat `git`*, under *The verbs*). - **A push or a Decline ends the Approve.** A new head ends its ask or its Approve, for a group every member's, and is said once on the pull request; the new head is asked again once its checks pass. A Decline merges nothing and is said once (ADR 0421 §6). @@ -1962,8 +1982,8 @@ later change of a kept data's form, not as a case of this milestone. module of the node; it never refuses the whole pass for one module's claim (ADR 0636 §3: a refused definition blocks new claims of that seat only). Met on 2026-11-08, when one claim problem refused a node's every module (issue 1966). A module held so is no silent leave-out: the node-engine writes the finding `module-held` naming the node, the - module and why, and the condition keeper keeps the condition (ADR 0564 §2) until the module is composed again, when - the node-engine clears the finding. How it is checked: test 55. + module and why, and the condition keeper keeps the condition (ADR 0564 §2) until the module is composed again or is + no longer wanted on that node, when the node-engine clears the finding. How it is checked: test 55. - **The order a node takes to composing its own.** One delivery group carries every part: the seat `mesh-module-assigner` on the assigner, the controller's verb `placement-export` (as it is) and the controller's pin only, the node-engine (its record and its own composer), the vault's `own`, the grants part's reads, and mesh-config's reading of @@ -2196,7 +2216,7 @@ From research 085's milestone the seat gains: of the revision view, ADR 0656), and `entry` and `difference` on `mesh-wants` (ADR 0656), with the events of *What each step says*. -**The seat `node-engine`**'s event `running.` carries, from research 085's milestone, the module, its +**The seat `node-engine`**'s event `running..` carries, from research 085's milestone, the module, its fingerprint and the digest of its build's manifest, so the node-engine of another node and the grants part decide a seat's pass from it (ADR 0660 §2; *Who holds a seat is applied by one rule*). No verb is added. @@ -2257,14 +2277,15 @@ Each is said on `mesh.seat.git.event.`, declared in the seat's definition still calls the retired acting tools, the old bridge's acts fail loudly and stay unperformed in the old gate's keys, and the switch carries them (*What the gate owes the forge*). A revert goes back in the other order: the gate, then the bridge, then the forge, each wanted only once the one after it in the forward order runs its reverted build, so - no part calls a verb nobody serves. + no part calls a verb nobody serves. ADR 0656 §3 stands beside it: the acting parts' new versions act first in their + own steps. Two things cover the gap while the forge is still on its old build and serves no verb of the seat: the + indexer starts on its kept definitions (below), and a check whose read through the seat `git` fails says `could not + run`, naming the forge, and is asked again on the forge module's `running` (*A check runs to its end*). - **A part that cannot read the forge keeps what it has, and says so.** The indexer, at its start, waits for the seat `git` at most 2 minutes; with no answer it judges from the last definitions it kept, says in its `_first_views` that it waits for `git` and since when, writes `view-waits` after five minutes, and reads the - definitions once the seat answers. Two minutes is longer than a forge's restart, and short enough that a start is - not held behind a forge down for good. How it is checked: in the indexer's whole-module test with a fixture forge on - its old build, which serves no verb of the seat, the indexer starts, judges from its kept definitions, and raises - `view-waits` after five minutes. + definitions once the seat answers; it then judges again every manifest it judged from kept definitions. Two minutes is longer than a forge's restart, and short enough that a start is + not held behind a forge down for good. How it is checked: test 54. - **How it is checked.** The seat definition's conformance fixtures hold one answer and one error of each verb and one of each event, and the forge module's tool tests answer each fixture; the forge bridge's, the checker's, the indexer's and the gate's unit fakes answer from the same fixtures; a call of an act verb from an account other than @@ -2570,9 +2591,10 @@ What milestone G's prepare found, and its test: grants composed from the holder's `callers` give the act verbs to the forge bridge's account alone, a call of one from another account is refused, and the forge module serves none of its retired acting tools (whole, forge bridge, grants part; live, the grants read with `grants_bindings`). -52. **Nothing holds an approved merge for another check.** Once the record that ends ADR 0551 and ADR 0548 §2 to §4, - §9, §10 and changes ADR 0534 §4 is accepted (to be accepted before the push): an approved merge into a repository - under a running check is handed at once and merged whatever that check says; neither the gate nor the forge module +52. **Nothing holds an approved merge for another check.** Once ADR 0661 (proposed, needs the operator's Approve; to + be accepted before the push), which ends ADR 0551 and ADR 0548 §2 to §4, §9, §10 and changes ADR 0534 §4, is + accepted: an approved merge into a repository under a running check is handed at once and merged whatever that + check says; a merge after a trunk move is not held for the carry; neither the gate nor the forge module declares `check-gates-merge`, the gate declares no `turns`, and the checker is granted no read of the gate's state (whole, gate, checker and forge module). 53. **What is owed is said, answered and kept.** An `owed` said while the forge bridge is stopped is performed once after @@ -2580,35 +2602,44 @@ What milestone G's prepare found, and its test: undecodable act is said `undecodable`; an act unanswered 15 minutes is listed by `stalled`; the switch with a merge handed and not yet merged opens no `forge-made` record; a merge made at the forge opens one, said as `transition` with why `forge-made`, and a later merge in its repository follows it; the checker's `mesh/check` is performed by - the bridge from the checker's `owed` (whole, gate and forge bridge). + the bridge from the checker's `owed`, and a checker status the forge refuses is recorded in the checker's own + state and listed in its stalled read (whole, gate, checker and forge bridge). 54. **One reader of the seat list per part, and one view machinery.** Every part that reads the seat list has the two tools of ADR 0636 §7 from the runtime and no wiring of its own; every part that takes a first view answers its - held and awaited revision in `_first_views`; the indexer with the forge on its old build, serving no verb of - the seat `git`, starts within 2 minutes on its kept definitions and raises `view-waits` after five (unit, mesh-sdk; - whole, indexer; live search). + held and awaited revision in `_first_views`. With the forge on its old build, serving no verb of the seat + `git`: the indexer starts within 2 minutes on its kept definitions, raises `view-waits` after five, and once the + seat answers judges again every manifest it judged from kept definitions; a check whose read through the seat + fails says `could not run` naming the forge, and is asked again on the forge module's `running` (unit, mesh-sdk; + whole, indexer and checker; live search). 55. **A claim problem holds one module, and says so.** A node with one module whose claim is refused applies the others; that module's have names why, and `mesh-conditions.conditions` reads its `module-held` condition until the - module is composed again, when it clears (whole, node-engine and condition keeper). + module is composed again or no longer wanted on that node, when it clears (whole, node-engine and condition + keeper). 56. **One rule picks a seat's holder, also with a node away** (ADR 0660 §1 to §3). With one node away whose last `running` named a build that no longer claims the seat, and one node that has said nothing since the parts' consumers began, the node-engine and the grants part each pick the same holder, the silent node counted by the - trunk's manifest; neither reads another node's have key, and neither calls a node (whole, node-engine and grants - part). + trunk's manifest; a consumer made after a node's last `running`, and the two parts' consumers made at different + times, still apply that build's manifest and pick the same holder; neither reads another node's have key, and + neither calls a node (whole, node-engine and grants part). 57. **The indexer reads every definition at its start.** Started with a definition on a contracts trunk that no event announced, the indexer judges the first manifest claiming that seat with it, and answers it in `seat-definitions` (whole, indexer). 58. **A check that could not run says so and is asked once more.** A check whose composer's node does not answer reads `could not run` and names that node in `mesh-delivery.show`; the claim is released; the node's `running` asks the check again once, and a second `running` of the same part at the same fingerprint asks no second check; a - `could not run` not asked again within 60 minutes raises `check-could-not-run` (whole, checker; live, read through + `could not run` not asked again within 60 minutes raises `check-could-not-run`, which reads cleared once the heads + are checked again, replaced, or their pull request closes (whole, checker; live, read through `mesh-delivery.show`). 59. **A session hears an accepted record** (issue 595). An Approve that accepts a record makes the knowledge base say `reviewed` with what was accepted, by the operator and with the warrant, and the tool runner's account hears it, so a session waiting with `mesh_wait` wakes on it (whole, knowledge base; live). 60. **The store's holder takes its own build when its seat is broken.** With a fixture holder of `oci-artifact-registry` that refuses every store, its new build is kept on the build node, named in the builder's done key, fetched by the - node-engine through `kept-artifact`, and once it runs the next build is stored through the seat, with no hand act - (whole, builder, build module and node-engine). + node-engine through `kept-artifact`, and once it runs the next build is stored through the seat, with no hand act; + a call of `kept-artifact` from an account other than a node-engine's is refused; it is refused for a module that + does not claim `oci-artifact-registry`; fetched bytes that differ from the done key's digest are not applied; an + artifact over the bus's value limit is fetched whole as an object; with the keeping node away, or the build swept, + the want waits and the have says why (whole, builder, build module and node-engine). ## What retires