Fixes issue 367 (a session's memory stays on the node where it was learned). Decision: novox/hq ADR 0343 (proposed, needs the operator's Approve); its home is design 36 §12 (hq PR on branch design/367-memory-follows-the-operator).
What
memory.go: the module's long-running process carries each project's memory folder, ~/.claude/projects/<project>/memory/, and nothing else of the home, through a new kept state memory (one key per home kind and file; the text, its digest, the digest it replaced, the node, the change time).
Picked up every 10 s; a file is read again only when its size or time changed.
Written on another node only where its file is still the version the change replaced, or absent; staged in the folder and renamed in, never through a symbolic link; the version replaced is kept first in memory-copies/ in the module's state directory (30 days).
Every write to the state is at the revision read (create / compare-and-set), and a change handed over late is applied as the state's current version, so two nodes never merge conflicts back and forth.
A file changed on two nodes becomes one file with both versions, the newer first, then a line naming each node and time, then the older; it is carried, and the operator is warned through seat:operator-channel.notify (warning, keyed per file, no path in the words; cleared when the file next changes without a conflict).
A removal is a marker value (never a deleted key, kept 30 days): an unchanged file elsewhere moves into the copies, a changed one is kept and carried again. A missing folder, a key the state lost, and a deleted key remove nothing.
Homes of the two kinds (the operator's own, an agent account's own) never take each other's memory; a confined agent in a home not its own carries nothing.
Not carried, and listed with why: symbolic links, files over 128 KiB, files with a key block, a signed token, a known token prefix or a password in an address (narrower than the SDK's check on purpose: notes name commit hashes).
claude_code_memory: read-only, fingerprints never text.
Manifest: state memory, tool claude_code_memory. README: the new section and the NATS row.
No tool writes a memory file or the state (issue 340's boundary).
Tested
New memory_test.go (temporary homes, a state in memory with the bus's watch semantics, never the live folders): carried unchanged and only once; only memory folders; exclusions listed; the two home kinds apart; conflict kept twice newer first, one warning, cleared on tidy, both versions in the copies; removal moves the unchanged file into the copies; a change outlives a removal in either order; missing folder / lost key / deleted key remove nothing; first carrying keeps both indexes and each node's own files; nothing written through a symbolic link; manifest declares the state.
On main the test file does not build (MemoryCarrier undefined); with the fix the package passes, also with -race -count=3 on the memory tests.
MESH_GATE=<controller built from mesh-controller main> MESH_CHECK_MODULES=claude-code sh merge-check.sh passes.
Acceptance
A memory file written on g14 appears unchanged on shanks within a minute (claude_code_memory on shanks lists it with g14's fingerprint), and a file changed on both before either carried it is kept twice in one file, newer first, with one warning on the operator's channel.
Rollout note
On the first look after delivery, every node carries its existing memory; where two nodes hold different versions of one file (MEMORY.md above all), both are kept in it and one warning per such file is sent. Nothing is lost: each node's prior version is also in its memory-copies/.
Left open, tracked: a value written straight into the state by a holder of a tool runner's credential is not told apart (issue 638).
Fixes issue 367 (a session's memory stays on the node where it was learned). Decision: novox/hq ADR 0343 (proposed, needs the operator's Approve); its home is design 36 §12 (hq PR on branch design/367-memory-follows-the-operator).
## What
- `memory.go`: the module's long-running process carries each project's memory folder, `~/.claude/projects/<project>/memory/`, and nothing else of the home, through a new kept state `memory` (one key per home kind and file; the text, its digest, the digest it replaced, the node, the change time).
- Picked up every 10 s; a file is read again only when its size or time changed.
- Written on another node only where its file is still the version the change replaced, or absent; staged in the folder and renamed in, never through a symbolic link; the version replaced is kept first in `memory-copies/` in the module's state directory (30 days).
- Every write to the state is at the revision read (create / compare-and-set), and a change handed over late is applied as the state's current version, so two nodes never merge conflicts back and forth.
- A file changed on two nodes becomes one file with both versions, the newer first, then a line naming each node and time, then the older; it is carried, and the operator is warned through `seat:operator-channel.notify` (warning, keyed per file, no path in the words; cleared when the file next changes without a conflict).
- A removal is a marker value (never a deleted key, kept 30 days): an unchanged file elsewhere moves into the copies, a changed one is kept and carried again. A missing folder, a key the state lost, and a deleted key remove nothing.
- Homes of the two kinds (the operator's own, an agent account's own) never take each other's memory; a confined agent in a home not its own carries nothing.
- Not carried, and listed with why: symbolic links, files over 128 KiB, files with a key block, a signed token, a known token prefix or a password in an address (narrower than the SDK's check on purpose: notes name commit hashes).
- `claude_code_memory`: read-only, fingerprints never text.
- Manifest: state `memory`, tool `claude_code_memory`. README: the new section and the NATS row.
- No tool writes a memory file or the state (issue 340's boundary).
## Tested
- New `memory_test.go` (temporary homes, a state in memory with the bus's watch semantics, never the live folders): carried unchanged and only once; only memory folders; exclusions listed; the two home kinds apart; conflict kept twice newer first, one warning, cleared on tidy, both versions in the copies; removal moves the unchanged file into the copies; a change outlives a removal in either order; missing folder / lost key / deleted key remove nothing; first carrying keeps both indexes and each node's own files; nothing written through a symbolic link; manifest declares the state.
- On main the test file does not build (`MemoryCarrier` undefined); with the fix the package passes, also with `-race -count=3` on the memory tests.
- `MESH_GATE=<controller built from mesh-controller main> MESH_CHECK_MODULES=claude-code sh merge-check.sh` passes.
## Acceptance
A memory file written on g14 appears unchanged on shanks within a minute (`claude_code_memory` on shanks lists it with g14's fingerprint), and a file changed on both before either carried it is kept twice in one file, newer first, with one warning on the operator's channel.
## Rollout note
On the first look after delivery, every node carries its existing memory; where two nodes hold different versions of one file (MEMORY.md above all), both are kept in it and one warning per such file is sent. Nothing is lost: each node's prior version is also in its `memory-copies/`.
Left open, tracked: a value written straight into the state by a holder of a tool runner's credential is not told apart (issue 638).
mesh/merge-gate head 6aa30eae66be merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 6aa30eae66be merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
A session started on another node began without what the sessions elsewhere had
learned, because Claude Code keeps project memory on one node. The module's own
process now carries the memory folders through its kept memory state: a change
is written elsewhere only over the version it replaced, a file changed on two
nodes keeps both (newer first) with a warning, and every replaced or removed
version is kept. No tool writes memory.
Deliverynovox/mesh-catalog@305601d25ef4 — delivered since 2026-10-13T14:30:36Z
Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
tier 0: claude-code
ace: receives claude-code
g14: receives claude-code
novox: receives claude-code
shanks: receives claude-code
claude-code keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
Machines
claude-code on ace: passed — healthy 1 times over 45s
claude-code on the rest running it: sent
Transitions
2026-10-13 14:20 (new) → proposed (announced): novox/mesh-catalog#276's head announced
2026-10-13 14:22 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-13 14:22 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-13 14:27 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 14:27 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 14:30 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-catalog@305601d25ef4` — **delivered** since 2026-10-13T14:30:36Z
**Delivery plan** — builds claude-code → ace, g14, novox, shanks; no bus step
- tier 0: claude-code
- ace: receives claude-code
- g14: receives claude-code
- novox: receives claude-code
- shanks: receives claude-code
- claude-code keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
**Machines**
- claude-code on ace: passed — healthy 1 times over 45s
- claude-code on the rest running it: sent
**Transitions**
- 2026-10-13 14:20 (new) → proposed (announced): novox/mesh-catalog#276's head announced
- 2026-10-13 14:22 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-13 14:22 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-13 14:27 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 14:27 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 14:30 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 6aa30eae
Judged: head 6aa30eae66be merged into trunk a84d0094a0a3; read beside it: mesh-controller 6f1a05366c1d, mesh-controller-main 6f1a05366c1d, mesh-host 0e92ef3d1b21, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
tier 0: claude-code
ace: receives claude-code
g14: receives claude-code
novox: receives claude-code
shanks: receives claude-code
claude-code keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on ace as build-1791899928332854741 (builds --log build-1791899928332854741).
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T13:42:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T13:42:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 46498 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.011s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.015s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.239s
testing modules/claude-code
ok claude-code/cmd/claude-code 3.818s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `6aa30eae`
Judged: head `6aa30eae66be` merged into trunk `a84d0094a0a3`; read beside it: mesh-controller `6f1a05366c1d`, mesh-controller-main `6f1a05366c1d`, mesh-host `0e92ef3d1b21`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds claude-code → ace, g14, novox, shanks; no bus step
- tier 0: claude-code
- ace: receives claude-code
- g14: receives claude-code
- novox: receives claude-code
- shanks: receives claude-code
- claude-code keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
- read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on ace as `build-1791899928332854741` (`builds --log build-1791899928332854741`).
```
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T13:42:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T13:42:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 46498 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.011s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.015s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.239s
testing modules/claude-code
ok claude-code/cmd/claude-code 3.818s
--- its merge-check-typescript.sh (typescript toolchain)
```
mesh/merge-gate head 0096beeff999 merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 0096beeff999 merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
The warning now opens with what the operator is needed for and names no path,
agent or session (ADR 0253's plain rule); clause references follow ADR 0343 as
reviewed.
mesh/merge-gate head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
A merged file could exceed the state's 256 KiB value; the newer now stays in the
file and the older is written beside it, each carried. A node that was away takes
the newest version when its own file holds no uncarried change (tested).
Merge check at 0096beef
Judged: head 0096beeff999 merged into trunk a84d0094a0a3; read beside it: mesh-controller 6f1a05366c1d, mesh-controller-main 6f1a05366c1d, mesh-host 0e92ef3d1b21, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
tier 0: claude-code
ace: receives claude-code
g14: receives claude-code
novox: receives claude-code
shanks: receives claude-code
claude-code keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on novox as build-1791900110428544412 (builds --log build-1791900110428544412).
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T13:42:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T13:42:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.012s
words: 12 retired words and 70 daemons' lines, none in the 46501 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.013s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.014s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.270s
testing modules/claude-code
ok claude-code/cmd/claude-code 6.345s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `0096beef`
Judged: head `0096beeff999` merged into trunk `a84d0094a0a3`; read beside it: mesh-controller `6f1a05366c1d`, mesh-controller-main `6f1a05366c1d`, mesh-host `0e92ef3d1b21`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds claude-code → ace, g14, novox, shanks; no bus step
- tier 0: claude-code
- ace: receives claude-code
- g14: receives claude-code
- novox: receives claude-code
- shanks: receives claude-code
- claude-code keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
- read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on novox as `build-1791900110428544412` (`builds --log build-1791900110428544412`).
```
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T13:42:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T13:42:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.012s
words: 12 retired words and 70 daemons' lines, none in the 46501 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.013s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.014s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.270s
testing modules/claude-code
ok claude-code/cmd/claude-code 6.345s
--- its merge-check-typescript.sh (typescript toolchain)
```
Two commits since the description, after the knowledge base's review of ADR 0343:
0096bee: the conflict warning follows ADR 0253's plain rule (explanation opens "Needs you: ...", no path, file name, agent or session; headline at most 60 characters), tested; a look the state does not answer carries nothing and says so (no 312 failed reads every 10 s while the bus is away).
910bc58: two versions too large to share one file (the state's value caps at 256 KiB) are kept in two: the newer in the file, the older beside it as <name>.older-from-<node><ext>, each carried, one warning; a text that would not fit one value once encoded is not carried and is listed. New tests: a node that was away while a file changed twice takes the newest version and keeps the replaced one; two large versions end as two files on both nodes.
merge-check.sh with MESH_CHECK_MODULES=claude-code and a controller from main passes locally; go test -race -count=2 of the package passes.
Two commits since the description, after the knowledge base's review of ADR 0343:
- 0096bee: the conflict warning follows ADR 0253's plain rule (explanation opens "Needs you: ...", no path, file name, agent or session; headline at most 60 characters), tested; a look the state does not answer carries nothing and says so (no 312 failed reads every 10 s while the bus is away).
- 910bc58: two versions too large to share one file (the state's value caps at 256 KiB) are kept in two: the newer in the file, the older beside it as `<name>.older-from-<node><ext>`, each carried, one warning; a text that would not fit one value once encoded is not carried and is listed. New tests: a node that was away while a file changed twice takes the newest version and keeps the replaced one; two large versions end as two files on both nodes.
merge-check.sh with MESH_CHECK_MODULES=claude-code and a controller from main passes locally; `go test -race -count=2` of the package passes.
Merge check at 910bc58f
Judged: head 910bc58fe01d merged into trunk a84d0094a0a3; read beside it: mesh-controller 6f1a05366c1d, mesh-controller-main 6f1a05366c1d, mesh-host 0e92ef3d1b21, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
tier 0: claude-code
ace: receives claude-code
g14: receives claude-code
novox: receives claude-code
shanks: receives claude-code
claude-code keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on shanks as build-1791900325049576514 (builds --log build-1791900325049576514).
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T14:02:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T14:02:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 46508 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.012s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.012s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.219s
testing modules/claude-code
ok claude-code/cmd/claude-code 6.112s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `910bc58f`
Judged: head `910bc58fe01d` merged into trunk `a84d0094a0a3`; read beside it: mesh-controller `6f1a05366c1d`, mesh-controller-main `6f1a05366c1d`, mesh-host `0e92ef3d1b21`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds claude-code → ace, g14, novox, shanks; no bus step
- tier 0: claude-code
- ace: receives claude-code
- g14: receives claude-code
- novox: receives claude-code
- shanks: receives claude-code
- claude-code keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
- read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on shanks as `build-1791900325049576514` (`builds --log build-1791900325049576514`).
```
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T14:02:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T14:02:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 46508 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.012s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.012s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.219s
testing modules/claude-code
ok claude-code/cmd/claude-code 6.112s
--- its merge-check-typescript.sh (typescript toolchain)
```
Independent review for issue 367 against ADR 0343 (version 14). Tests pass niced with -race (ok claude-code/cmd/claude-code 7.0s), and the clause-by-clause shape matches the record. Four faults were confirmed with probe tests in a throwaway clone (not pushed); two touch data safety and one touches ADR 0201 §13.
Path traversal from the state writes outside ~/.claude/projects/*/memory/ (blocking). Take (memory.go:714, 728) only checks that the key equals MemoryKey(kind, v.Project, v.Path), but the key is just base64 of project/path, so a writer of the state chooses both. memoryFolder/readLocal/writeLocal/removeLocal/keep (memory.go:238-396) join v.Project unchecked; inside() guards only path against its own folder. Probe: a value with project ../../../escaped wrote <root>/shanks/escaped/memory/x.md on the other node. On a confined node it is worse: Files.through (homefs.go:73) routes any path outside the agent's home to the plain filesystem as the runtime's (operator's) account, so a value in the agent half can write into the operator's home, which breaks ADR 0343 §4. keep() (memory.go:343) also writes copies outside memory-copies/ the same way. Fix: refuse a project that is not one directory entry (entryName, homefs.go) and a path that is absolute, has a .. or empty element, or a NUL; apply it in Take, apply and lookAt before any filesystem call; add a test.
A read failure is carried as a removal (blocking, data safety). scan (memory.go:263-293) returns nil from the WalkDir callback on a walk error, a Lstat error or a ReadFile error, so that file is in neither files nor not; Look (memory.go:560-567) then treats it as gone in an existing folder and removedHere publishes a removal marker; every other node moves its copy into memory-copies/. Probe: chmod 000 on one file on g14 removed it from shanks. Through the helper (homefs.go:222-236) a failed readdir of the memory folder after a successful lstat calls fn(root, d, err), which full == dir swallows, so a helper hiccup on the confined node removes every carried file of that project on every agent-kind node. Fix: record any error under a folder and judge no removal in that project on that look (or put the file in not).
Text never screened goes into the state (blocking, ADR 0343 §2 and ADR 0201 §13). apply reads the local file with readLocal and carries it without memoryProblem: the removal branch (memory.go:768-773) puts the local text back, and the conflict branch (memory.go:798) goes to conflictBeside (memory.go:652-668), which puts the newer side unscreened when the merge is refused for a secret or size. Probes: a local file holding a ghp_... token (listed not carried) was put into the state verbatim (a) after another node removed the file and (b) after another node changed it. Same path lets a file over 128 KiB through. Fix: screen every text before every put/Update (one place in put and in both conflict writers) and keep a file that fails the screen out of the state, listed.
A conflict whose local write fails is merged again on every look (should fix). conflict (memory.go:627-634) updates the state before writeLocal; if that fails (for example keep cannot write memory-copies/), the record is not updated and the next look merges the local text into the already-merged value again. Probe: five looks gave a state value with five merge lines (1796 bytes), carried to every node each time. No warning is sent then (warn comes after the write), so the operator does not see it. Fix: write locally (and keep) first, or record the merged digest as carried for that key when the state write succeeded, and say the failure loudly.
Smaller points, not blocking:
memory.go:308-332 vs 383: apply decides on one read and writeLocal re-reads and renames; a change by the program between them is kept in the copies but no conflict is raised. Low.
A file that is not valid UTF-8 is carried with the digest of the raw bytes but text mangled by JSON (memory.go:592), so every other node refuses it at memory.go:744 and it is never listed as not carried (§2 says every file not carried is listed).
apply's removal case (memory.go:760): a node whose local file is still its last-carried version but not the removal's base carries the file back; conservative, but §9 reads as moving it. Say which is meant.
Conflict warnings: one per conflict, keyed by noticeKey (memory.go:433-472), and the merged file does not re-trigger on the other nodes (marker check memory.go:790). Correct, apart from point 4.
Home kinds and novox: HomeKind (memory.go:129-138) and the prefix and kind checks in Take are correct; point 1 is the only way across.
The tool answers only fingerprints and no event carries text; Say lines carry project and path, never text. Correct.
The manifest declares the memory state with no bucket-wide lifetime, so the earlier knowledge-base remark (do not expire live values) holds.
Verdict: request changes at 910bc58fe01d989097ca21920e9eaa0558e0650e
Independent review for issue 367 against ADR 0343 (version 14). Tests pass niced with -race (`ok claude-code/cmd/claude-code 7.0s`), and the clause-by-clause shape matches the record. Four faults were confirmed with probe tests in a throwaway clone (not pushed); two touch data safety and one touches ADR 0201 §13.
1. Path traversal from the state writes outside ~/.claude/projects/*/memory/ (blocking). `Take` (memory.go:714, 728) only checks that the key equals `MemoryKey(kind, v.Project, v.Path)`, but the key is just base64 of `project/path`, so a writer of the state chooses both. `memoryFolder`/`readLocal`/`writeLocal`/`removeLocal`/`keep` (memory.go:238-396) join `v.Project` unchecked; `inside()` guards only `path` against its own folder. Probe: a value with project `../../../escaped` wrote `<root>/shanks/escaped/memory/x.md` on the other node. On a confined node it is worse: `Files.through` (homefs.go:73) routes any path outside the agent's home to the plain filesystem as the runtime's (operator's) account, so a value in the `agent` half can write into the operator's home, which breaks ADR 0343 §4. `keep()` (memory.go:343) also writes copies outside `memory-copies/` the same way. Fix: refuse a project that is not one directory entry (`entryName`, homefs.go) and a path that is absolute, has a `..` or empty element, or a NUL; apply it in `Take`, `apply` and `lookAt` before any filesystem call; add a test.
2. A read failure is carried as a removal (blocking, data safety). `scan` (memory.go:263-293) returns nil from the WalkDir callback on a walk error, a Lstat error or a ReadFile error, so that file is in neither `files` nor `not`; `Look` (memory.go:560-567) then treats it as gone in an existing folder and `removedHere` publishes a removal marker; every other node moves its copy into `memory-copies/`. Probe: chmod 000 on one file on g14 removed it from shanks. Through the helper (homefs.go:222-236) a failed `readdir` of the memory folder after a successful lstat calls fn(root, d, err), which `full == dir` swallows, so a helper hiccup on the confined node removes every carried file of that project on every agent-kind node. Fix: record any error under a folder and judge no removal in that project on that look (or put the file in `not`).
3. Text never screened goes into the state (blocking, ADR 0343 §2 and ADR 0201 §13). `apply` reads the local file with `readLocal` and carries it without `memoryProblem`: the removal branch (memory.go:768-773) puts the local text back, and the conflict branch (memory.go:798) goes to `conflictBeside` (memory.go:652-668), which puts the newer side unscreened when the merge is refused for a secret or size. Probes: a local file holding a `ghp_...` token (listed not carried) was put into the state verbatim (a) after another node removed the file and (b) after another node changed it. Same path lets a file over 128 KiB through. Fix: screen every text before every `put`/`Update` (one place in `put` and in both conflict writers) and keep a file that fails the screen out of the state, listed.
4. A conflict whose local write fails is merged again on every look (should fix). `conflict` (memory.go:627-634) updates the state before `writeLocal`; if that fails (for example `keep` cannot write `memory-copies/`), the record is not updated and the next look merges the local text into the already-merged value again. Probe: five looks gave a state value with five merge lines (1796 bytes), carried to every node each time. No warning is sent then (warn comes after the write), so the operator does not see it. Fix: write locally (and keep) first, or record the merged digest as carried for that key when the state write succeeded, and say the failure loudly.
Smaller points, not blocking:
- memory.go:308-332 vs 383: `apply` decides on one read and `writeLocal` re-reads and renames; a change by the program between them is kept in the copies but no conflict is raised. Low.
- A file that is not valid UTF-8 is carried with the digest of the raw bytes but text mangled by JSON (memory.go:592), so every other node refuses it at memory.go:744 and it is never listed as not carried (§2 says every file not carried is listed).
- apply's removal case (memory.go:760): a node whose local file is still its last-carried version but not the removal's base carries the file back; conservative, but §9 reads as moving it. Say which is meant.
- Conflict warnings: one per conflict, keyed by `noticeKey` (memory.go:433-472), and the merged file does not re-trigger on the other nodes (marker check memory.go:790). Correct, apart from point 4.
- Home kinds and novox: `HomeKind` (memory.go:129-138) and the prefix and kind checks in `Take` are correct; point 1 is the only way across.
- The tool answers only fingerprints and no event carries text; `Say` lines carry project and path, never text. Correct.
- The manifest declares the `memory` state with no bucket-wide lifetime, so the earlier knowledge-base remark (do not expire live values) holds.
mesh/merge-gate head 305601d25ef4 merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 305601d25ef4 merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
A value in the state could name a project such as ../../.. and be written outside
the memory folder; a file or folder that could not be read was carried as a
removal; text kept over a removal or merged in a conflict reached the state
unscreened; and a conflict whose file could not be written was merged into the
state again on every look. Names are judged before any file is touched, what
cannot be read is kept and listed, every state write is screened, a conflict is
written to the file before the state and warns once when it cannot be, a file
changed between reading and writing is not written over, non-UTF-8 text is not
carried, and a node that was away takes a removal it holds no change against
(ADR 0343 §9, as clarified).
Merge check at 305601d2
Judged: head 305601d25ef4 merged into trunk a84d0094a0a3; read beside it: mesh-controller 6f1a05366c1d, mesh-controller-main 6f1a05366c1d, mesh-host 0e92ef3d1b21, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
tier 0: claude-code
ace: receives claude-code
g14: receives claude-code
novox: receives claude-code
shanks: receives claude-code
claude-code keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on ace as build-1791901209019351906 (builds --log build-1791901209019351906).
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T14:02:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T14:02:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 46565 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.012s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.012s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.231s
testing modules/claude-code
ok claude-code/cmd/claude-code 5.846s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `305601d2`
Judged: head `305601d25ef4` merged into trunk `a84d0094a0a3`; read beside it: mesh-controller `6f1a05366c1d`, mesh-controller-main `6f1a05366c1d`, mesh-host `0e92ef3d1b21`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds claude-code → ace, g14, novox, shanks; no bus step
- tier 0: claude-code
- ace: receives claude-code
- g14: receives claude-code
- novox: receives claude-code
- shanks: receives claude-code
- claude-code keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before claude-code is judged on its first machine
- read by no module's build: modules/claude-code/README.md, modules/claude-code/cmd/claude-code/memory_test.go
Run by the build seat on ace as `build-1791901209019351906` (`builds --log build-1791901209019351906`).
```
--- the gate: claude-code
note: claude-code uses the seat "operator-channel", which no module declares and the mesh does not define among the manifests given; registration judges it against the whole catalogue, and passing the declaring module's manifest too judges it here
claude-code: ok, 48 tool(s), keeps state servers, holdings, config, proposals, questions, memory, reads claude-licence-manager.bindings, keeps agent-home (valuable); WARNING: settings ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T14:02:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T14:02:41Z
modules the change moves:
- claude-code (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): claude-code
notes:
- modules/claude-code/README.md and modules/claude-code/cmd/claude-code/memory_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 46565 strings of 717 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.012s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/secrets 0.012s
secrets: no setting can replace a vault secret in a mergeable file (1 known, nested)
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 66, as kept
ok git.novox.be/novox/mesh-catalog/checks/deps 0.231s
testing modules/claude-code
ok claude-code/cmd/claude-code 5.846s
--- its merge-check-typescript.sh (typescript toolchain)
```
Re-review of review 205's findings against ADR 0343 (version 14). I recreated last time's probe tests in a throwaway clone, added three more, and ran them niced with -race against this head. All pass; the four original probes failed at 910bc58f. The module's own tests pass too (ok claude-code/cmd/claude-code 5.7s).
Path traversal is fixed. NameProblem and badName (memory.go:401-424) run in Take, apply, readLocal, writeLocal, removeLocal, keep and write before any file is touched. Probe: values naming ../../../escaped, .., a/b, ../x.md, /abs.md and a//b.md wrote nothing anywhere.
A read failure no longer becomes a removal. scan (memory.go:271-368) marks a file it cannot look at or read as unseen, and leaves a folder it cannot list whole out of folders; Look skips both (memory.go:705). Probes: a chmod-000 file, an unlistable memory folder and an unlistable subfolder on g14 each removed nothing on shanks.
Every state write is screened. write (memory.go:626-654) checks name, key, text and digest. apply's default branch refuses to merge a local text that would not be carried (memory.go:985-990). Probes: a local ghp_... file was not put into the state after a removal or after a change elsewhere, and it stayed on shanks unchanged.
A conflict is no longer merged again and again. The file is written first, then the state by compare-and-set (memory.go:768-778). Probe: five looks with an unwritable memory-copies/ left the state at 311 bytes with no merge line, and sent exactly one warning. Once the folder was writable again, both nodes ended on the same file holding both versions.
Non-UTF-8 text is now listed as not carried and is not carried (probe passes). The redefined §9 (a node still on its last carried version takes the removal) matches §9's wording, and TestARemovalReachesANodeThatWasAway covers it.
Should fix, in this PR or a small follow-up:
The new "could not be written" warning (unwritten, memory.go:787-809) is never cleared. No clear is ever sent, so its "resolved" line never fires, and m.failed is never reset. Once the folder is writable again, the operator keeps a stale warning until they dismiss it. Clear it, and reset failed[key], on the next successful write of that file.
The same warning's explanation always says "Two versions of one of its files are waiting to be kept together". When it comes from apply's plain write (memory.go:973), no conflict exists and the words are wrong. Word it for both cases.
This warning goes beyond ADR 0343 §8, which names only the conflict warning. It is in the spirit of "failure must be loud"; record it in design 36 §12 or in the record.
Not blocking: writeLocal and removeLocal now re-check the digest they read, which narrows the race with the program to the moment between that check and the rename.
Verdict: approve at 305601d25ef451cc8c0c31331092343ee5ba32b9
Re-review of review 205's findings against ADR 0343 (version 14). I recreated last time's probe tests in a throwaway clone, added three more, and ran them niced with -race against this head. All pass; the four original probes failed at 910bc58f. The module's own tests pass too (`ok claude-code/cmd/claude-code 5.7s`).
1. Path traversal is fixed. `NameProblem` and `badName` (memory.go:401-424) run in `Take`, `apply`, `readLocal`, `writeLocal`, `removeLocal`, `keep` and `write` before any file is touched. Probe: values naming `../../../escaped`, `..`, `a/b`, `../x.md`, `/abs.md` and `a//b.md` wrote nothing anywhere.
2. A read failure no longer becomes a removal. `scan` (memory.go:271-368) marks a file it cannot look at or read as unseen, and leaves a folder it cannot list whole out of `folders`; `Look` skips both (memory.go:705). Probes: a chmod-000 file, an unlistable memory folder and an unlistable subfolder on g14 each removed nothing on shanks.
3. Every state write is screened. `write` (memory.go:626-654) checks name, key, text and digest. `apply`'s default branch refuses to merge a local text that would not be carried (memory.go:985-990). Probes: a local `ghp_...` file was not put into the state after a removal or after a change elsewhere, and it stayed on shanks unchanged.
4. A conflict is no longer merged again and again. The file is written first, then the state by compare-and-set (memory.go:768-778). Probe: five looks with an unwritable `memory-copies/` left the state at 311 bytes with no merge line, and sent exactly one warning. Once the folder was writable again, both nodes ended on the same file holding both versions.
5. Non-UTF-8 text is now listed as not carried and is not carried (probe passes). The redefined §9 (a node still on its last carried version takes the removal) matches §9's wording, and `TestARemovalReachesANodeThatWasAway` covers it.
Should fix, in this PR or a small follow-up:
- The new "could not be written" warning (`unwritten`, memory.go:787-809) is never cleared. No clear is ever sent, so its "resolved" line never fires, and `m.failed` is never reset. Once the folder is writable again, the operator keeps a stale warning until they dismiss it. Clear it, and reset `failed[key]`, on the next successful write of that file.
- The same warning's explanation always says "Two versions of one of its files are waiting to be kept together". When it comes from `apply`'s plain write (memory.go:973), no conflict exists and the words are wrong. Word it for both cases.
- This warning goes beyond ADR 0343 §8, which names only the conflict warning. It is in the spirit of "failure must be loud"; record it in design 36 §12 or in the record.
Not blocking: `writeLocal` and `removeLocal` now re-check the digest they read, which narrows the race with the program to the moment between that check and the rename.
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.
Fixes issue 367 (a session's memory stays on the node where it was learned). Decision: novox/hq ADR 0343 (proposed, needs the operator's Approve); its home is design 36 §12 (hq PR on branch design/367-memory-follows-the-operator).
What
memory.go: the module's long-running process carries each project's memory folder,~/.claude/projects/<project>/memory/, and nothing else of the home, through a new kept statememory(one key per home kind and file; the text, its digest, the digest it replaced, the node, the change time).memory-copies/in the module's state directory (30 days).seat:operator-channel.notify(warning, keyed per file, no path in the words; cleared when the file next changes without a conflict).claude_code_memory: read-only, fingerprints never text.memory, toolclaude_code_memory. README: the new section and the NATS row.Tested
memory_test.go(temporary homes, a state in memory with the bus's watch semantics, never the live folders): carried unchanged and only once; only memory folders; exclusions listed; the two home kinds apart; conflict kept twice newer first, one warning, cleared on tidy, both versions in the copies; removal moves the unchanged file into the copies; a change outlives a removal in either order; missing folder / lost key / deleted key remove nothing; first carrying keeps both indexes and each node's own files; nothing written through a symbolic link; manifest declares the state.MemoryCarrierundefined); with the fix the package passes, also with-race -count=3on the memory tests.MESH_GATE=<controller built from mesh-controller main> MESH_CHECK_MODULES=claude-code sh merge-check.shpasses.Acceptance
A memory file written on g14 appears unchanged on shanks within a minute (
claude_code_memoryon shanks lists it with g14's fingerprint), and a file changed on both before either carried it is kept twice in one file, newer first, with one warning on the operator's channel.Rollout note
On the first look after delivery, every node carries its existing memory; where two nodes hold different versions of one file (MEMORY.md above all), both are kept in it and one warning per such file is sent. Nothing is lost: each node's prior version is also in its
memory-copies/.Left open, tracked: a value written straight into the state by a holder of a tool runner's credential is not told apart (issue 638).
Delivery
novox/mesh-catalog@305601d25ef4— delivered since 2026-10-13T14:30:36ZDelivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
Machines
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
6aa30eaeJudged: head
6aa30eae66bemerged into trunka84d0094a0a3; read beside it: mesh-controller6f1a05366c1d, mesh-controller-main6f1a05366c1d, mesh-host0e92ef3d1b21, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
Run by the build seat on ace as
build-1791899928332854741(builds --log build-1791899928332854741).Merge check at
0096beefJudged: head
0096beeff999merged into trunka84d0094a0a3; read beside it: mesh-controller6f1a05366c1d, mesh-controller-main6f1a05366c1d, mesh-host0e92ef3d1b21, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
Run by the build seat on novox as
build-1791900110428544412(builds --log build-1791900110428544412).Two commits since the description, after the knowledge base's review of ADR 0343:
0096bee: the conflict warning follows ADR 0253's plain rule (explanation opens "Needs you: ...", no path, file name, agent or session; headline at most 60 characters), tested; a look the state does not answer carries nothing and says so (no 312 failed reads every 10 s while the bus is away).910bc58: two versions too large to share one file (the state's value caps at 256 KiB) are kept in two: the newer in the file, the older beside it as<name>.older-from-<node><ext>, each carried, one warning; a text that would not fit one value once encoded is not carried and is listed. New tests: a node that was away while a file changed twice takes the newest version and keeps the replaced one; two large versions end as two files on both nodes.merge-check.sh with MESH_CHECK_MODULES=claude-code and a controller from main passes locally;
go test -race -count=2of the package passes.Merge check at
910bc58fJudged: head
910bc58fe01dmerged into trunka84d0094a0a3; read beside it: mesh-controller6f1a05366c1d, mesh-controller-main6f1a05366c1d, mesh-host0e92ef3d1b21, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
Run by the build seat on shanks as
build-1791900325049576514(builds --log build-1791900325049576514).Verdict: request changes at
910bc58fe0Independent review for issue 367 against ADR 0343 (version 14). Tests pass niced with -race (
ok claude-code/cmd/claude-code 7.0s), and the clause-by-clause shape matches the record. Four faults were confirmed with probe tests in a throwaway clone (not pushed); two touch data safety and one touches ADR 0201 §13.Path traversal from the state writes outside ~/.claude/projects/*/memory/ (blocking).
Take(memory.go:714, 728) only checks that the key equalsMemoryKey(kind, v.Project, v.Path), but the key is just base64 ofproject/path, so a writer of the state chooses both.memoryFolder/readLocal/writeLocal/removeLocal/keep(memory.go:238-396) joinv.Projectunchecked;inside()guards onlypathagainst its own folder. Probe: a value with project../../../escapedwrote<root>/shanks/escaped/memory/x.mdon the other node. On a confined node it is worse:Files.through(homefs.go:73) routes any path outside the agent's home to the plain filesystem as the runtime's (operator's) account, so a value in theagenthalf can write into the operator's home, which breaks ADR 0343 §4.keep()(memory.go:343) also writes copies outsidememory-copies/the same way. Fix: refuse a project that is not one directory entry (entryName, homefs.go) and a path that is absolute, has a..or empty element, or a NUL; apply it inTake,applyandlookAtbefore any filesystem call; add a test.A read failure is carried as a removal (blocking, data safety).
scan(memory.go:263-293) returns nil from the WalkDir callback on a walk error, a Lstat error or a ReadFile error, so that file is in neitherfilesnornot;Look(memory.go:560-567) then treats it as gone in an existing folder andremovedHerepublishes a removal marker; every other node moves its copy intomemory-copies/. Probe: chmod 000 on one file on g14 removed it from shanks. Through the helper (homefs.go:222-236) a failedreaddirof the memory folder after a successful lstat calls fn(root, d, err), whichfull == dirswallows, so a helper hiccup on the confined node removes every carried file of that project on every agent-kind node. Fix: record any error under a folder and judge no removal in that project on that look (or put the file innot).Text never screened goes into the state (blocking, ADR 0343 §2 and ADR 0201 §13).
applyreads the local file withreadLocaland carries it withoutmemoryProblem: the removal branch (memory.go:768-773) puts the local text back, and the conflict branch (memory.go:798) goes toconflictBeside(memory.go:652-668), which puts the newer side unscreened when the merge is refused for a secret or size. Probes: a local file holding aghp_...token (listed not carried) was put into the state verbatim (a) after another node removed the file and (b) after another node changed it. Same path lets a file over 128 KiB through. Fix: screen every text before everyput/Update(one place inputand in both conflict writers) and keep a file that fails the screen out of the state, listed.A conflict whose local write fails is merged again on every look (should fix).
conflict(memory.go:627-634) updates the state beforewriteLocal; if that fails (for examplekeepcannot writememory-copies/), the record is not updated and the next look merges the local text into the already-merged value again. Probe: five looks gave a state value with five merge lines (1796 bytes), carried to every node each time. No warning is sent then (warn comes after the write), so the operator does not see it. Fix: write locally (and keep) first, or record the merged digest as carried for that key when the state write succeeded, and say the failure loudly.Smaller points, not blocking:
applydecides on one read andwriteLocalre-reads and renames; a change by the program between them is kept in the copies but no conflict is raised. Low.noticeKey(memory.go:433-472), and the merged file does not re-trigger on the other nodes (marker check memory.go:790). Correct, apart from point 4.HomeKind(memory.go:129-138) and the prefix and kind checks inTakeare correct; point 1 is the only way across.Saylines carry project and path, never text. Correct.memorystate with no bucket-wide lifetime, so the earlier knowledge-base remark (do not expire live values) holds.Merge check at
305601d2Judged: head
305601d25ef4merged into trunka84d0094a0a3; read beside it: mesh-controller6f1a05366c1d, mesh-controller-main6f1a05366c1d, mesh-host0e92ef3d1b21, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: claude-code)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds claude-code → ace, g14, novox, shanks; no bus step
Run by the build seat on ace as
build-1791901209019351906(builds --log build-1791901209019351906).Verdict: approve at
305601d25eRe-review of review 205's findings against ADR 0343 (version 14). I recreated last time's probe tests in a throwaway clone, added three more, and ran them niced with -race against this head. All pass; the four original probes failed at
910bc58f. The module's own tests pass too (ok claude-code/cmd/claude-code 5.7s).NameProblemandbadName(memory.go:401-424) run inTake,apply,readLocal,writeLocal,removeLocal,keepandwritebefore any file is touched. Probe: values naming../../../escaped,..,a/b,../x.md,/abs.mdanda//b.mdwrote nothing anywhere.scan(memory.go:271-368) marks a file it cannot look at or read as unseen, and leaves a folder it cannot list whole out offolders;Lookskips both (memory.go:705). Probes: a chmod-000 file, an unlistable memory folder and an unlistable subfolder on g14 each removed nothing on shanks.write(memory.go:626-654) checks name, key, text and digest.apply's default branch refuses to merge a local text that would not be carried (memory.go:985-990). Probes: a localghp_...file was not put into the state after a removal or after a change elsewhere, and it stayed on shanks unchanged.memory-copies/left the state at 311 bytes with no merge line, and sent exactly one warning. Once the folder was writable again, both nodes ended on the same file holding both versions.TestARemovalReachesANodeThatWasAwaycovers it.Should fix, in this PR or a small follow-up:
unwritten, memory.go:787-809) is never cleared. No clear is ever sent, so its "resolved" line never fires, andm.failedis never reset. Once the folder is writable again, the operator keeps a stale warning until they dismiss it. Clear it, and resetfailed[key], on the next successful write of that file.apply's plain write (memory.go:973), no conflict exists and the words are wrong. Word it for both cases.Not blocking:
writeLocalandremoveLocalnow re-check the digest they read, which narrows the race with the program to the moment between that check and the rename.