What: runProvisioner keeps a standing per consumer, as the Go copy of the loop (postgres, keycloak) does, and announces it (novox/hq ADR 0224):
provisioner.failing after five minutes of create, check (holds) or secret failures with no success between, naming the consumer, its machine, the class (credentials-rejected, unreachable, secret-unreadable, refused; an adapter's optional classify is believed), the error with the password removed, since and attempts; again every 15 minutes while it lasts.
provisioner.recovered on the first success after it, for a consumer no longer asked for (why: no longer asked for) or retired (why: retired), and on the first success per consumer since start (why: first-success).
The announce option now defaults to the SDK's emit. On main no TypeScript provider passed announce, so none said provisioner.retirement either; with this default every provider says all three events on the next build, with no catalogue change. A failed publish is said in the journal and never fails the act.
classOf is exported and reads the same words as the Go loop, plus Node's own (ECONNREFUSED, ENOTFOUND, ...). A holds timeout classes as unreachable.
Why: issue 272 - half the mesh's providers (object store, SQL server, document store, mail server, forge, analytics, ...) failed their consumers in their journal alone, so status and the operator never heard (the shape of issue 179).
The withdrawal brake the issue also names is ADR 0230's retirement waiting, already said as provisioner.retirement (waiting); with the default announce it now reaches the bus too. No separate withdrawal-braked class: the Go loop has none either since ADR 0230.
How tested: test/standing.test.ts ports the Go standing_test.go (announced after failingAfter not before, said again, recovered; one success restarts the run; a failing check; an unreadable secret; no longer asked for; retired; classes by words; adapter classify; first success once; default announce goes through emit). It fails on main (no standing, no classOf). test/retire.test.ts now collects only retirement events (its assert inside announce would otherwise throw into the loop's catch). merge-check.sh green locally (47 TS tests, Go binding).
Fixes issue 272.
What: runProvisioner keeps a standing per consumer, as the Go copy of the loop (postgres, keycloak) does, and announces it (novox/hq ADR 0224):
- provisioner.failing after five minutes of create, check (holds) or secret failures with no success between, naming the consumer, its machine, the class (credentials-rejected, unreachable, secret-unreadable, refused; an adapter's optional classify is believed), the error with the password removed, since and attempts; again every 15 minutes while it lasts.
- provisioner.recovered on the first success after it, for a consumer no longer asked for (why: no longer asked for) or retired (why: retired), and on the first success per consumer since start (why: first-success).
- The announce option now defaults to the SDK's emit. On main no TypeScript provider passed announce, so none said provisioner.retirement either; with this default every provider says all three events on the next build, with no catalogue change. A failed publish is said in the journal and never fails the act.
- classOf is exported and reads the same words as the Go loop, plus Node's own (ECONNREFUSED, ENOTFOUND, ...). A holds timeout classes as unreachable.
Why: issue 272 - half the mesh's providers (object store, SQL server, document store, mail server, forge, analytics, ...) failed their consumers in their journal alone, so status and the operator never heard (the shape of issue 179).
The withdrawal brake the issue also names is ADR 0230's retirement waiting, already said as provisioner.retirement (waiting); with the default announce it now reaches the bus too. No separate withdrawal-braked class: the Go loop has none either since ADR 0230.
How tested: test/standing.test.ts ports the Go standing_test.go (announced after failingAfter not before, said again, recovered; one success restarts the run; a failing check; an unreadable secret; no longer asked for; retired; classes by words; adapter classify; first success once; default announce goes through emit). It fails on main (no standing, no classOf). test/retire.test.ts now collects only retirement events (its assert inside announce would otherwise throw into the loop's catch). merge-check.sh green locally (47 TS tests, Go binding).
Fixes issue 272.
mesh/merge-gate head d49d3a14d82a merged into trunk c1ae30b0a2c8 — pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change…
mesh/repo-check head d49d3a14d82a merged into trunk c1ae30b0a2c8 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
No provider on runProvisioner passed announce, and the loop had no standing per
consumer, so the object store, the SQL and document stores, the mail server and
the others failed their consumers in their journals alone. The loop now keeps the
Go copy's standing (failing after five minutes, again every fifteen, recovered on
success, when unasked or retired, and on the first success since start) and says
it through the SDK's emit unless given another announce.
Deliverynovox/mesh-sdk@d49d3a14d82a — delivered since 2026-10-13T06:38:01Z
Delivery plan — builds mesh-sdk, sent nowhere; no bus step
tier 0: mesh-sdk
Machines
mesh-sdk on every machine running it: sent
Transitions
2026-10-13 06:26 (new) → proposed (announced): novox/mesh-sdk#24's head announced
2026-10-13 06:29 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-13 06:29 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-13 06:37 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-13 06:37 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
2026-10-13 06:38 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-sdk@d49d3a14d82a` — **delivered** since 2026-10-13T06:38:01Z
**Delivery plan** — builds mesh-sdk, sent nowhere; no bus step
- tier 0: mesh-sdk
**Machines**
- mesh-sdk on every machine running it: sent
**Transitions**
- 2026-10-13 06:26 (new) → proposed (announced): novox/mesh-sdk#24's head announced
- 2026-10-13 06:29 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-13 06:29 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-13 06:37 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-13 06:37 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
- 2026-10-13 06:38 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Merge check at d49d3a14
Judged: head d49d3a14d82a merged into trunk c1ae30b0a2c8; read beside it: mesh-catalog bc3b59227f46, mesh-controller cd6757f1e3c8, mesh-controller-main a688dcff394b, mesh-host 7a41e120635b, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-sdk)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
Delivery plan — builds mesh-sdk, sent nowhere; no bus step
tier 0: mesh-sdk
Run by the build seat on g14 as build-1791872811619938380 (builds --log build-1791872811619938380).
# [provisioner:postgres-database] contributions file readable again
# [provisioner:postgres-database] /tmp/prov-vPPjbN/postgres-database.json is for s3-bucket, not postgres-database; nothing applied or removed until it can be read
# [provisioner:postgres-database] contributions file readable again
# [provisioner:postgres-database] /tmp/prov-vPPjbN/postgres-database.json has no given list; nothing applied or removed until it can be read
# [provisioner:postgres-database] contributions file readable again
# [provisioner:postgres-database] RETIREMENT WAITS FOR A PERSON: the mesh no longer asks for 2 of the 2 consumer(s) this provider holds (mesh_anchor_forge, mesh_anchor_mail), more than 3, or more than 50% of the 2 held. Nothing is retired until `retire approve` or `retire reject` (novox/hq ADR 0230)
# [provisioner:postgres-database] could not announce provisioner.retirement waiting: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:postgres-database] retirement of mesh_anchor_forge, mesh_anchor_mail approved by test: both unassigned on purpose
# [provisioner:postgres-database] could not announce provisioner.retirement approved: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:postgres-database] mesh_anchor_forge: no longer in /tmp/prov-vPPjbN/postgres-database.json; RETIRED, MARK ONLY — this provider cannot disable a consumer (its adapter has no retire), so mesh_anchor_forge KEEPS ITS ACCESS and its data until a person deletes it with `cleanup delete`. Nothing was changed in the backend (novox/hq ADR 0230): approved by test: both unassigned on purpose
# [provisioner:postgres-database] mesh_anchor_mail: no longer in /tmp/prov-vPPjbN/postgres-database.json; RETIRED, MARK ONLY — this provider cannot disable a consumer (its adapter has no retire), so mesh_anchor_mail KEEPS ITS ACCESS and its data until a person deletes it with `cleanup delete`. Nothing was changed in the backend (novox/hq ADR 0230): approved by test: both unassigned on purpose
# [provisioner:postgres-database] could not announce provisioner.retirement retired: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# Subtest: provisioner removes nothing while its contributions file cannot be used (novox/hq issue 241)
ok 21 - provisioner removes nothing while its contributions file cannot be used (novox/hq issue 241)
---
duration_ms: 1322.633747
type: 'test'
...
# [provisioner:cache] could not announce provisioner.recovered: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:cache] webapp-anchor: the backend no longer holds it; applying again
# [provisioner:cache] could not announce provisioner.recovered: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# Subtest: provisioner applies again what the backend no longer holds, and trusts memory without holds
ok 22 - provisioner applies again what the backend no longer holds, and trusts memory without holds
---
duration_ms: 213.209494
type: 'test'
...
# [provisioner:cache] could not announce provisioner.recovered: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:cache] webapp-anchor: could not check the backend, will ask again: Error: connection refused
# [provisioner:cache] webapp-anchor: could not check the backend, will ask again: Error: connection refused
# [provisioner:cache] webapp-anchor: could not check the backend, will ask again: Error: connection refused
# Subtest: provisioner keeps a consumer applied when the backend cannot be asked
ok 23 - provisioner keeps a consumer applied when the backend cannot be asked
---
duration_ms: 71.315884
type: 'test'
...
# Subtest: provisioner backs off when create does not satisfy holds, and says so
ok 24 - provisioner backs off when create does not satisfy holds, and says so
---
duration_ms: 403.27589
type: 'test'
...
# Subtest: provisioner treats a failed or hung holds as could-not-ask, and never logs the password
ok 25 - provisioner treats a failed or hung holds as could-not-ask, and never logs the password
---
duration_ms: 404.551406
type: 'test'
...
# Subtest: provisioner stops a verify pass at a timeout, and retries a failed re-apply at the next check
ok 26 - provisioner stops a verify pass at a timeout, and retries a failed re-apply at the next check
---
duration_ms: 304.885907
type: 'test'
...
# Subtest: modules can SERVE: a real async tool, loaded and invoked over the broker
ok 27 - modules can SERVE: a real async tool, loaded and invoked over the broker
---
duration_ms: 53.631022
type: 'test'
...
# Subtest: serving refuses one module exposing two tools of the same name
ok 28 - serving refuses one module exposing two tools of the same name
---
duration_ms: 0.472457
type: 'test'
...
# Subtest: events: a module emits, a listener and the audit sink (\#) both receive it, with metadata
ok 29 - events: a module emits, a listener and the audit sink (\#) both receive it, with metadata
---
duration_ms: 0.646785
type: 'test'
...
# Subtest: each registration is given the environment asked for it, and sees no other's (ADR 0192)
ok 30 - each registration is given the environment asked for it, and sees no other's (ADR 0192)
---
duration_ms: 0.410831
type: 'test'
...
# Subtest: a consumer failed for minutes is announced, naming it, its machine and the class, again while it lasts, and recovered
ok 31 - a consumer failed for minutes is announced, naming it, its machine and the class, again while it lasts, and recovered
---
duration_ms: 594.298356
type: 'test'
...
# Subtest: one success between failures starts the run again
ok 32 - one success between failures starts the run again
---
duration_ms: 192.843979
type: 'test'
...
# Subtest: a check that keeps failing is a failure too
ok 33 - a check that keeps failing is a failure too
---
duration_ms: 189.37735
type: 'test'
...
# Subtest: an unreadable secret is announced as such
ok 34 - an unreadable secret is announced as such
---
duration_ms: 165.40332
type: 'test'
...
# Subtest: a consumer no longer asked for is no longer failing
ok 35 - a consumer no longer asked for is no longer failing
---
duration_ms: 175.901499
type: 'test'
...
# Subtest: a failing consumer the mesh stopped asking for is said recovered when it is retired
ok 36 - a failing consumer the mesh stopped asking for is said recovered when it is retired
---
duration_ms: 238.080621
type: 'test'
...
# Subtest: an error is classed by its words
ok 37 - an error is classed by its words
---
duration_ms: 0.503295
type: 'test'
...
# Subtest: an adapter that classes its own errors is believed
ok 38 - an adapter that classes its own errors is believed
---
duration_ms: 171.41142
type: 'test'
...
# Subtest: the first success since start is announced once
ok 39 - the first success since start is announced once
---
duration_ms: 112.889868
type: 'test'
...
# Subtest: without announce the standing is said through the SDK's emit
ok 40 - without announce the standing is said through the SDK's emit
---
duration_ms: 176.797994
type: 'test'
...
# Subtest: a bundle on stdio lists its tools and answers a call, a seat's verb among them
ok 41 - a bundle on stdio lists its tools and answers a call, a seat's verb among them
---
duration_ms: 105.395356
type: 'test'
...
# Subtest: a bundle served as its module lists the seat's verbs as the seat's, whichever registered first (ADR 0193)
ok 42 - a bundle served as its module lists the seat's verbs as the seat's, whichever registered first (ADR 0193)
---
duration_ms: 83.382388
type: 'test'
...
# Subtest: a launched tool's emit is asked of the runtime, and the call answers once the runtime has published (ADR 0193)
ok 43 - a launched tool's emit is asked of the runtime, and the call answers once the runtime has published (ADR 0193)
---
duration_ms: 83.945495
type: 'test'
...
# Subtest: a launched bundle subscribes as it is imported, handles events the runtime hands it, and asks through it (ADR 0198)
ok 44 - a launched bundle subscribes as it is imported, handles events the runtime hands it, and asks through it (ADR 0198)
---
duration_ms: 3196.064799
type: 'test'
...
# Subtest: a subscribe the runtime refused leaves no handler behind, so one event is handled once (novox/hq issue 276)
ok 45 - a subscribe the runtime refused leaves no handler behind, so one event is handled once (novox/hq issue 276)
---
duration_ms: 84.530946
type: 'test'
...
# Subtest: a launched bundle watches its state as it is imported, has the current values before it goes on, and keeps state through the runtime (ADR 0201)
ok 46 - a launched bundle watches its state as it is imported, has the current values before it goes on, and keeps state through the runtime (ADR 0201)
---
duration_ms: 266.445066
type: 'test'
...
# Subtest: topics match as the runtime's do: * one segment, \# and ** any number
ok 47 - topics match as the runtime's do: * one segment, \# and ** any number
---
duration_ms: 2.051712
type: 'test'
...
1..47
# tests 47
# suites 0
# pass 47
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 3895.281992
NOT TESTED HERE: go/ (the Go binding) — the Go toolchain runs it from go/merge-check.sh
NOT TESTED HERE: python/, rust/, c/ — no toolchain of theirs runs a merge check
--- its go/merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-sdk/go 0.092s
ok git.novox.be/novox/mesh-sdk/go/asker 0.002s
ok git.novox.be/novox/mesh-sdk/go/asks 0.002s
ok git.novox.be/novox/mesh-sdk/go/cli 1.030s
? git.novox.be/novox/mesh-sdk/go/example [no test files]
ok git.novox.be/novox/mesh-sdk/go/health 0.002s
ok git.novox.be/novox/mesh-sdk/go/outward 0.040s
**Merge check** at `d49d3a14`
Judged: head `d49d3a14d82a` merged into trunk `c1ae30b0a2c8`; read beside it: mesh-catalog `bc3b59227f46`, mesh-controller `cd6757f1e3c8`, mesh-controller-main `a688dcff394b`, mesh-host `7a41e120635b`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-sdk)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
**Delivery plan** — builds mesh-sdk, sent nowhere; no bus step
- tier 0: mesh-sdk
Run by the build seat on g14 as `build-1791872811619938380` (`builds --log build-1791872811619938380`).
```
# [provisioner:postgres-database] contributions file readable again
# [provisioner:postgres-database] /tmp/prov-vPPjbN/postgres-database.json is for s3-bucket, not postgres-database; nothing applied or removed until it can be read
# [provisioner:postgres-database] contributions file readable again
# [provisioner:postgres-database] /tmp/prov-vPPjbN/postgres-database.json has no given list; nothing applied or removed until it can be read
# [provisioner:postgres-database] contributions file readable again
# [provisioner:postgres-database] RETIREMENT WAITS FOR A PERSON: the mesh no longer asks for 2 of the 2 consumer(s) this provider holds (mesh_anchor_forge, mesh_anchor_mail), more than 3, or more than 50% of the 2 held. Nothing is retired until `retire approve` or `retire reject` (novox/hq ADR 0230)
# [provisioner:postgres-database] could not announce provisioner.retirement waiting: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:postgres-database] retirement of mesh_anchor_forge, mesh_anchor_mail approved by test: both unassigned on purpose
# [provisioner:postgres-database] could not announce provisioner.retirement approved: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:postgres-database] mesh_anchor_forge: no longer in /tmp/prov-vPPjbN/postgres-database.json; RETIRED, MARK ONLY — this provider cannot disable a consumer (its adapter has no retire), so mesh_anchor_forge KEEPS ITS ACCESS and its data until a person deletes it with `cleanup delete`. Nothing was changed in the backend (novox/hq ADR 0230): approved by test: both unassigned on purpose
# [provisioner:postgres-database] mesh_anchor_mail: no longer in /tmp/prov-vPPjbN/postgres-database.json; RETIRED, MARK ONLY — this provider cannot disable a consumer (its adapter has no retire), so mesh_anchor_mail KEEPS ITS ACCESS and its data until a person deletes it with `cleanup delete`. Nothing was changed in the backend (novox/hq ADR 0230): approved by test: both unassigned on purpose
# [provisioner:postgres-database] could not announce provisioner.retirement retired: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# Subtest: provisioner removes nothing while its contributions file cannot be used (novox/hq issue 241)
ok 21 - provisioner removes nothing while its contributions file cannot be used (novox/hq issue 241)
---
duration_ms: 1322.633747
type: 'test'
...
# [provisioner:cache] could not announce provisioner.recovered: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:cache] webapp-anchor: the backend no longer holds it; applying again
# [provisioner:cache] could not announce provisioner.recovered: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# Subtest: provisioner applies again what the backend no longer holds, and trusts memory without holds
ok 22 - provisioner applies again what the backend no longer holds, and trusts memory without holds
---
duration_ms: 213.209494
type: 'test'
...
# [provisioner:cache] could not announce provisioner.recovered: Error: no broker is bound — the tool runtime or provisioner host must call useBroker() before module code reaches the mesh
# [provisioner:cache] webapp-anchor: could not check the backend, will ask again: Error: connection refused
# [provisioner:cache] webapp-anchor: could not check the backend, will ask again: Error: connection refused
# [provisioner:cache] webapp-anchor: could not check the backend, will ask again: Error: connection refused
# Subtest: provisioner keeps a consumer applied when the backend cannot be asked
ok 23 - provisioner keeps a consumer applied when the backend cannot be asked
---
duration_ms: 71.315884
type: 'test'
...
# Subtest: provisioner backs off when create does not satisfy holds, and says so
ok 24 - provisioner backs off when create does not satisfy holds, and says so
---
duration_ms: 403.27589
type: 'test'
...
# Subtest: provisioner treats a failed or hung holds as could-not-ask, and never logs the password
ok 25 - provisioner treats a failed or hung holds as could-not-ask, and never logs the password
---
duration_ms: 404.551406
type: 'test'
...
# Subtest: provisioner stops a verify pass at a timeout, and retries a failed re-apply at the next check
ok 26 - provisioner stops a verify pass at a timeout, and retries a failed re-apply at the next check
---
duration_ms: 304.885907
type: 'test'
...
# Subtest: modules can SERVE: a real async tool, loaded and invoked over the broker
ok 27 - modules can SERVE: a real async tool, loaded and invoked over the broker
---
duration_ms: 53.631022
type: 'test'
...
# Subtest: serving refuses one module exposing two tools of the same name
ok 28 - serving refuses one module exposing two tools of the same name
---
duration_ms: 0.472457
type: 'test'
...
# Subtest: events: a module emits, a listener and the audit sink (\#) both receive it, with metadata
ok 29 - events: a module emits, a listener and the audit sink (\#) both receive it, with metadata
---
duration_ms: 0.646785
type: 'test'
...
# Subtest: each registration is given the environment asked for it, and sees no other's (ADR 0192)
ok 30 - each registration is given the environment asked for it, and sees no other's (ADR 0192)
---
duration_ms: 0.410831
type: 'test'
...
# Subtest: a consumer failed for minutes is announced, naming it, its machine and the class, again while it lasts, and recovered
ok 31 - a consumer failed for minutes is announced, naming it, its machine and the class, again while it lasts, and recovered
---
duration_ms: 594.298356
type: 'test'
...
# Subtest: one success between failures starts the run again
ok 32 - one success between failures starts the run again
---
duration_ms: 192.843979
type: 'test'
...
# Subtest: a check that keeps failing is a failure too
ok 33 - a check that keeps failing is a failure too
---
duration_ms: 189.37735
type: 'test'
...
# Subtest: an unreadable secret is announced as such
ok 34 - an unreadable secret is announced as such
---
duration_ms: 165.40332
type: 'test'
...
# Subtest: a consumer no longer asked for is no longer failing
ok 35 - a consumer no longer asked for is no longer failing
---
duration_ms: 175.901499
type: 'test'
...
# Subtest: a failing consumer the mesh stopped asking for is said recovered when it is retired
ok 36 - a failing consumer the mesh stopped asking for is said recovered when it is retired
---
duration_ms: 238.080621
type: 'test'
...
# Subtest: an error is classed by its words
ok 37 - an error is classed by its words
---
duration_ms: 0.503295
type: 'test'
...
# Subtest: an adapter that classes its own errors is believed
ok 38 - an adapter that classes its own errors is believed
---
duration_ms: 171.41142
type: 'test'
...
# Subtest: the first success since start is announced once
ok 39 - the first success since start is announced once
---
duration_ms: 112.889868
type: 'test'
...
# Subtest: without announce the standing is said through the SDK's emit
ok 40 - without announce the standing is said through the SDK's emit
---
duration_ms: 176.797994
type: 'test'
...
# Subtest: a bundle on stdio lists its tools and answers a call, a seat's verb among them
ok 41 - a bundle on stdio lists its tools and answers a call, a seat's verb among them
---
duration_ms: 105.395356
type: 'test'
...
# Subtest: a bundle served as its module lists the seat's verbs as the seat's, whichever registered first (ADR 0193)
ok 42 - a bundle served as its module lists the seat's verbs as the seat's, whichever registered first (ADR 0193)
---
duration_ms: 83.382388
type: 'test'
...
# Subtest: a launched tool's emit is asked of the runtime, and the call answers once the runtime has published (ADR 0193)
ok 43 - a launched tool's emit is asked of the runtime, and the call answers once the runtime has published (ADR 0193)
---
duration_ms: 83.945495
type: 'test'
...
# Subtest: a launched bundle subscribes as it is imported, handles events the runtime hands it, and asks through it (ADR 0198)
ok 44 - a launched bundle subscribes as it is imported, handles events the runtime hands it, and asks through it (ADR 0198)
---
duration_ms: 3196.064799
type: 'test'
...
# Subtest: a subscribe the runtime refused leaves no handler behind, so one event is handled once (novox/hq issue 276)
ok 45 - a subscribe the runtime refused leaves no handler behind, so one event is handled once (novox/hq issue 276)
---
duration_ms: 84.530946
type: 'test'
...
# Subtest: a launched bundle watches its state as it is imported, has the current values before it goes on, and keeps state through the runtime (ADR 0201)
ok 46 - a launched bundle watches its state as it is imported, has the current values before it goes on, and keeps state through the runtime (ADR 0201)
---
duration_ms: 266.445066
type: 'test'
...
# Subtest: topics match as the runtime's do: * one segment, \# and ** any number
ok 47 - topics match as the runtime's do: * one segment, \# and ** any number
---
duration_ms: 2.051712
type: 'test'
...
1..47
# tests 47
# suites 0
# pass 47
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 3895.281992
NOT TESTED HERE: go/ (the Go binding) — the Go toolchain runs it from go/merge-check.sh
NOT TESTED HERE: python/, rust/, c/ — no toolchain of theirs runs a merge check
--- its go/merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-sdk/go 0.092s
ok git.novox.be/novox/mesh-sdk/go/asker 0.002s
ok git.novox.be/novox/mesh-sdk/go/asks 0.002s
ok git.novox.be/novox/mesh-sdk/go/cli 1.030s
? git.novox.be/novox/mesh-sdk/go/example [no test files]
ok git.novox.be/novox/mesh-sdk/go/health 0.002s
ok git.novox.be/novox/mesh-sdk/go/outward 0.040s
```
runProvisioner now carries the Go loop's standing per consumer (trouble map) and announces provisioner.failing once a run of create/holds/secret failures passes failingAfterMs (default FAILING_AFTER_MS, five minutes) with no success between, repeating every sayAgainEveryMs (default fifteen minutes) while it lasts, and provisioner.recovered on the first success after it, on a consumer no longer asked for, on retirement, and on the first success per consumer since start — matching ADR 0224 clauses 1-2 and the retirement-moves-the-recovery clause 3 (the recovered(as, "retired") call inside retireAll). The body carries consumer, the consumer's machine (node), class, error, since and attempts, as clause 1 asks. classOf reads the same words as the Go loop's classOf (credentials-rejected, unreachable, secret-unreadable, refused) plus Node's own error codes, and an adapter's classify is preferred when it answers, per clause 4. I ran the Go copy's postgres/keycloak behaviour side by side against test/standing.test.ts's cases and they match.
announce now defaults to (event, body) => emit(event, body), and emit (src/events/index.ts) only stamps ADR 0042 headers (x-event-id, x-source, x-node, x-time) and republishes the caller's body verbatim — it adds nothing of its own, so the only way a secret reaches the bus is if the body passed to announce carries one. Every body built for provisioner.failing goes through failed(), which receives text already run through scrub(err, password) (password and its URL-encoded form replaced with ***) at both call sites that can see a real backend error (the holds catch and the create catch); the one call site that does not scrub (the secret-read failure) never had the password in scope, since the file could not be opened. provisioner.recovered and provisioner.retirement bodies carry only consumer/node/class/since/attempts/why — no error text, hence no password. test/standing.test.ts's first case asserts error does not match the literal password. No secret travels, per ADR 0224 clause 5.
Confirmed by running the suite myself (clone at /var/tmp/review-sdk24, npm ci && tsc --noEmit && npm test): all 47 tests pass at d49d3a14d8, including the 10 new cases in test/standing.test.ts and the adjusted test/retire.test.ts (which now filters announce to RETIREMENT_EVENT only, so standing events it does not expect don't throw into the loop's catch). I then checked out the merge base c1ae30b0a2c81fe8b5945fda403423f4d4ada923's src/ and ran the head's test/standing.test.ts against it unmodified: it fails immediately — SyntaxError: The requested module '../dist/provisioner/index.js' does not provide an export named 'CLASS_CREDENTIALS' — because main's runProvisioner has no standing, no classOf and no FAILING_EVENT/RECOVERED_EVENT exports. sh merge-check.sh is green at the head: 47 of 47 TS tests pass, and the Go binding's go test ./... passes. The PR's own bot comments show the same: mesh/merge-gate: PASS — "every machine composes with the change as it did without (4 of 4 compose)", and mesh/repo-check: PASS — "its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)", both read at d49d3a14d8.
The 11 TypeScript bundles that declare a provisioner/index.js entrypoint in mesh-catalog are gitea, influxdb, mesh-vault, mosquitto, minio, mssql, mongodb, mailu, umami, redis and showcase. Of these, 10 actually call runProvisioner from this SDK and will gain the two new events once rebuilt against 0.1.14; showcase's provisioner/index.ts calls a provisioner({...}) function that does not exist in this SDK at all (only runProvisioner is exported) — it is already broken against the current SDK, independent of this PR, and will not pick up anything from this fix. The version a provider's build carries is not a literal pin anywhere in mesh-catalog: mesh-tools/Dockerfile's ARG MESH_SDK=@novox/mesh-sdk@latest is only the fallback for a manual/unlinked build. mesh-tools/module.json's build.on names {"arg": "MESH_SDK", "module": "mesh-sdk", "artifact": "lib"}, so the controller fills that build argument from whatever mesh-sdk's lib package artifact currently is — mesh-sdk/module.json declares exactly one artifact, lib (kind package, language typescript), built from this repo's own package.json, now at version 0.1.14. So once this PR merges and the controller builds and publishes mesh-sdk's lib artifact as 0.1.14, mesh-tools' toolchain and runtime images rebuild pinned to @novox/mesh-sdk@0.1.14 (the Dockerfile's own comment: "a new release is a new argument, this layer runs again" — novox/hq issue 212), and per the module graph the planner then rebuilds every TypeScript bundle compiled in that toolchain (issue 211) — the 10 real providers among the 11. No catalogue change is needed for them to run 0.1.14; what should happen is the ordinary merge and walk, and then mesh-controller.status/plans should be checked to confirm the walk actually reached mesh-tools and, after it, the 10 provider modules, not just mesh-sdk itself. This is a finding, not a blocker for this PR.
One small thing I noticed but did not block on: README.md's new paragraph says provisioner.recovered fires "on the first success for each consumer since the process started," which matches the code and ADR 0224 clause 2 exactly.
Verdict: approve at d49d3a14d82a5dd83f7d9e683a969287e96875cd
1. runProvisioner now carries the Go loop's standing per consumer (`trouble` map) and announces `provisioner.failing` once a run of create/holds/secret failures passes `failingAfterMs` (default `FAILING_AFTER_MS`, five minutes) with no success between, repeating every `sayAgainEveryMs` (default fifteen minutes) while it lasts, and `provisioner.recovered` on the first success after it, on a consumer no longer asked for, on retirement, and on the first success per consumer since start — matching ADR 0224 clauses 1-2 and the retirement-moves-the-recovery clause 3 (the `recovered(as, "retired")` call inside `retireAll`). The body carries consumer, the consumer's machine (`node`), `class`, `error`, `since` and `attempts`, as clause 1 asks. `classOf` reads the same words as the Go loop's `classOf` (credentials-rejected, unreachable, secret-unreadable, refused) plus Node's own error codes, and an adapter's `classify` is preferred when it answers, per clause 4. I ran the Go copy's postgres/keycloak behaviour side by side against `test/standing.test.ts`'s cases and they match.
2. `announce` now defaults to `(event, body) => emit(event, body)`, and `emit` (src/events/index.ts) only stamps ADR 0042 headers (x-event-id, x-source, x-node, x-time) and republishes the caller's body verbatim — it adds nothing of its own, so the only way a secret reaches the bus is if the body passed to `announce` carries one. Every body built for `provisioner.failing` goes through `failed()`, which receives `text` already run through `scrub(err, password)` (password and its URL-encoded form replaced with `***`) at both call sites that can see a real backend error (the `holds` catch and the `create` catch); the one call site that does not scrub (the secret-read failure) never had the password in scope, since the file could not be opened. `provisioner.recovered` and `provisioner.retirement` bodies carry only consumer/node/class/since/attempts/why — no error text, hence no password. `test/standing.test.ts`'s first case asserts `error` does not match the literal password. No secret travels, per ADR 0224 clause 5.
3. Confirmed by running the suite myself (clone at /var/tmp/review-sdk24, npm ci && tsc --noEmit && npm test): all 47 tests pass at d49d3a14d82a5dd83f7d9e683a969287e96875cd, including the 10 new cases in test/standing.test.ts and the adjusted test/retire.test.ts (which now filters `announce` to RETIREMENT_EVENT only, so standing events it does not expect don't throw into the loop's catch). I then checked out the merge base c1ae30b0a2c81fe8b5945fda403423f4d4ada923's src/ and ran the head's test/standing.test.ts against it unmodified: it fails immediately — `SyntaxError: The requested module '../dist/provisioner/index.js' does not provide an export named 'CLASS_CREDENTIALS'` — because main's runProvisioner has no standing, no classOf and no FAILING_EVENT/RECOVERED_EVENT exports. `sh merge-check.sh` is green at the head: 47 of 47 TS tests pass, and the Go binding's `go test ./...` passes. The PR's own bot comments show the same: `mesh/merge-gate`: **PASS** — "every machine composes with the change as it did without (4 of 4 compose)", and `mesh/repo-check`: **PASS** — "its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)", both read at d49d3a14d82a5dd83f7d9e683a969287e96875cd.
4. The 11 TypeScript bundles that declare a `provisioner/index.js` entrypoint in mesh-catalog are gitea, influxdb, mesh-vault, mosquitto, minio, mssql, mongodb, mailu, umami, redis and showcase. Of these, 10 actually call `runProvisioner` from this SDK and will gain the two new events once rebuilt against 0.1.14; showcase's provisioner/index.ts calls a `provisioner({...})` function that does not exist in this SDK at all (only `runProvisioner` is exported) — it is already broken against the current SDK, independent of this PR, and will not pick up anything from this fix. The version a provider's build carries is not a literal pin anywhere in mesh-catalog: mesh-tools/Dockerfile's `ARG MESH_SDK=@novox/mesh-sdk@latest` is only the fallback for a manual/unlinked build. mesh-tools/module.json's `build.on` names `{"arg": "MESH_SDK", "module": "mesh-sdk", "artifact": "lib"}`, so the controller fills that build argument from whatever mesh-sdk's `lib` package artifact currently is — mesh-sdk/module.json declares exactly one artifact, `lib` (kind package, language typescript), built from this repo's own package.json, now at version 0.1.14. So once this PR merges and the controller builds and publishes mesh-sdk's `lib` artifact as 0.1.14, mesh-tools' toolchain and runtime images rebuild pinned to `@novox/mesh-sdk@0.1.14` (the Dockerfile's own comment: "a new release is a new argument, this layer runs again" — novox/hq issue 212), and per the module graph the planner then rebuilds every TypeScript bundle compiled in that toolchain (issue 211) — the 10 real providers among the 11. No catalogue change is needed for them to run 0.1.14; what should happen is the ordinary merge and walk, and then `mesh-controller.status`/`plans` should be checked to confirm the walk actually reached mesh-tools and, after it, the 10 provider modules, not just mesh-sdk itself. This is a finding, not a blocker for this PR.
One small thing I noticed but did not block on: README.md's new paragraph says `provisioner.recovered` fires "on the first success for each consumer since the process started," which matches the code and ADR 0224 clause 2 exactly.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What: runProvisioner keeps a standing per consumer, as the Go copy of the loop (postgres, keycloak) does, and announces it (novox/hq ADR 0224):
Why: issue 272 - half the mesh's providers (object store, SQL server, document store, mail server, forge, analytics, ...) failed their consumers in their journal alone, so status and the operator never heard (the shape of issue 179).
The withdrawal brake the issue also names is ADR 0230's retirement waiting, already said as provisioner.retirement (waiting); with the default announce it now reaches the bus too. No separate withdrawal-braked class: the Go loop has none either since ADR 0230.
How tested: test/standing.test.ts ports the Go standing_test.go (announced after failingAfter not before, said again, recovered; one success restarts the run; a failing check; an unreadable secret; no longer asked for; retired; classes by words; adapter classify; first success once; default announce goes through emit). It fails on main (no standing, no classOf). test/retire.test.ts now collects only retirement events (its assert inside announce would otherwise throw into the loop's catch). merge-check.sh green locally (47 TS tests, Go binding).
Fixes issue 272.
Delivery
novox/mesh-sdk@d49d3a14d82a— delivered since 2026-10-13T06:38:01ZDelivery plan — builds mesh-sdk, sent nowhere; no bus step
Machines
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
d49d3a14Judged: head
d49d3a14d82amerged into trunkc1ae30b0a2c8; read beside it: mesh-catalogbc3b59227f46, mesh-controllercd6757f1e3c8, mesh-controller-maina688dcff394b, mesh-host7a41e120635b, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-sdk)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)Delivery plan — builds mesh-sdk, sent nowhere; no bus step
Run by the build seat on g14 as
build-1791872811619938380(builds --log build-1791872811619938380).Verdict: approve at
d49d3a14d8runProvisioner now carries the Go loop's standing per consumer (
troublemap) and announcesprovisioner.failingonce a run of create/holds/secret failures passesfailingAfterMs(defaultFAILING_AFTER_MS, five minutes) with no success between, repeating everysayAgainEveryMs(default fifteen minutes) while it lasts, andprovisioner.recoveredon the first success after it, on a consumer no longer asked for, on retirement, and on the first success per consumer since start — matching ADR 0224 clauses 1-2 and the retirement-moves-the-recovery clause 3 (therecovered(as, "retired")call insideretireAll). The body carries consumer, the consumer's machine (node),class,error,sinceandattempts, as clause 1 asks.classOfreads the same words as the Go loop'sclassOf(credentials-rejected, unreachable, secret-unreadable, refused) plus Node's own error codes, and an adapter'sclassifyis preferred when it answers, per clause 4. I ran the Go copy's postgres/keycloak behaviour side by side againsttest/standing.test.ts's cases and they match.announcenow defaults to(event, body) => emit(event, body), andemit(src/events/index.ts) only stamps ADR 0042 headers (x-event-id, x-source, x-node, x-time) and republishes the caller's body verbatim — it adds nothing of its own, so the only way a secret reaches the bus is if the body passed toannouncecarries one. Every body built forprovisioner.failinggoes throughfailed(), which receivestextalready run throughscrub(err, password)(password and its URL-encoded form replaced with***) at both call sites that can see a real backend error (theholdscatch and thecreatecatch); the one call site that does not scrub (the secret-read failure) never had the password in scope, since the file could not be opened.provisioner.recoveredandprovisioner.retirementbodies carry only consumer/node/class/since/attempts/why — no error text, hence no password.test/standing.test.ts's first case assertserrordoes not match the literal password. No secret travels, per ADR 0224 clause 5.Confirmed by running the suite myself (clone at /var/tmp/review-sdk24, npm ci && tsc --noEmit && npm test): all 47 tests pass at
d49d3a14d8, including the 10 new cases in test/standing.test.ts and the adjusted test/retire.test.ts (which now filtersannounceto RETIREMENT_EVENT only, so standing events it does not expect don't throw into the loop's catch). I then checked out the merge base c1ae30b0a2c81fe8b5945fda403423f4d4ada923's src/ and ran the head's test/standing.test.ts against it unmodified: it fails immediately —SyntaxError: The requested module '../dist/provisioner/index.js' does not provide an export named 'CLASS_CREDENTIALS'— because main's runProvisioner has no standing, no classOf and no FAILING_EVENT/RECOVERED_EVENT exports.sh merge-check.shis green at the head: 47 of 47 TS tests pass, and the Go binding'sgo test ./...passes. The PR's own bot comments show the same:mesh/merge-gate: PASS — "every machine composes with the change as it did without (4 of 4 compose)", andmesh/repo-check: PASS — "its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)", both read atd49d3a14d8.The 11 TypeScript bundles that declare a
provisioner/index.jsentrypoint in mesh-catalog are gitea, influxdb, mesh-vault, mosquitto, minio, mssql, mongodb, mailu, umami, redis and showcase. Of these, 10 actually callrunProvisionerfrom this SDK and will gain the two new events once rebuilt against 0.1.14; showcase's provisioner/index.ts calls aprovisioner({...})function that does not exist in this SDK at all (onlyrunProvisioneris exported) — it is already broken against the current SDK, independent of this PR, and will not pick up anything from this fix. The version a provider's build carries is not a literal pin anywhere in mesh-catalog: mesh-tools/Dockerfile'sARG MESH_SDK=@novox/mesh-sdk@latestis only the fallback for a manual/unlinked build. mesh-tools/module.json'sbuild.onnames{"arg": "MESH_SDK", "module": "mesh-sdk", "artifact": "lib"}, so the controller fills that build argument from whatever mesh-sdk'slibpackage artifact currently is — mesh-sdk/module.json declares exactly one artifact,lib(kind package, language typescript), built from this repo's own package.json, now at version 0.1.14. So once this PR merges and the controller builds and publishes mesh-sdk'slibartifact as 0.1.14, mesh-tools' toolchain and runtime images rebuild pinned to@novox/mesh-sdk@0.1.14(the Dockerfile's own comment: "a new release is a new argument, this layer runs again" — novox/hq issue 212), and per the module graph the planner then rebuilds every TypeScript bundle compiled in that toolchain (issue 211) — the 10 real providers among the 11. No catalogue change is needed for them to run 0.1.14; what should happen is the ordinary merge and walk, and thenmesh-controller.status/plansshould be checked to confirm the walk actually reached mesh-tools and, after it, the 10 provider modules, not just mesh-sdk itself. This is a finding, not a blocker for this PR.One small thing I noticed but did not block on: README.md's new paragraph says
provisioner.recoveredfires "on the first success for each consumer since the process started," which matches the code and ADR 0224 clause 2 exactly.