0.1.14: the provider loop says a consumer it keeps failing on the bus, and its recovery (hq ADR 0224, issue 272) #24

Merged
mesh-admin merged 1 commits from fix/272-the-provider-loop-says-its-standing into main 2026-10-13 06:37:30 +00:00
Contributor

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-admin added 1 commit 2026-10-13 06:26:47 +00:00
0.1.14: the provider loop says a consumer it keeps failing on the bus, and its recovery (hq ADR 0224, 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)…
mesh/delivery delivered
d49d3a14d8
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.
Author
Contributor

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.

<!-- 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`.
Author
Contributor

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 ```
mesh-admin left a comment
Author
Contributor

Verdict: approve at d49d3a14d8

  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 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.

  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.

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.
mesh-admin merged commit a0fc82fdcd into main 2026-10-13 06:37:30 +00:00
mesh-admin deleted branch fix/272-the-provider-loop-says-its-standing 2026-10-13 06:37:31 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-sdk#24