Surfaced on 2026-07-31 during the #16167 cutover of this machine to the container topology. Memory Core and Knowledge Base are now containerized behind a loopback ingress; Neural Link stays host-side stdio and is expected to (it possesses the browser, so it is host-edge by nature — see #15173 for the streamable-HTTP direction).
With MC containerized against a bind-mounted canonical plane, every host-side Neural Link becomes a concurrent writer on the same SQLite file as the containerized Memory Core.
Live evidence during the cutover — two NL processes from one seat, holding the canonical graph with write handles (u = read/write):
They respawned on harness restart. Nine seats means nine potential NL writers against one containerized reader/writer.
Live latest-open sweep at 2026-07-31T01:20Z: no open issue covers an NL recorder opt-out or its role as a plane writer.
The Problem
1. There is no opt-out. A repo-wide sweep for a gate (NEO_NL_*RECORD*, recorderEnabled, disableRecorder, and neighbours) returns nothing. The only way to stop NL from writing the canonical graph today is to unset or blank NEO_MEMORY_DB_PATH, which works only because RecorderService.mjs:87 degrades on a falsy path:
logger.warn('[RecorderService] memoryCoreDbPath not configured. Disabling logging.');
Disabling a writer by emptying a path is a side effect, not an interface. It cannot be expressed per-seat without also breaking anything else that reads the same config leaf, and it reads as a misconfiguration rather than an intent.
2. The runbook's writer census cannot see it.#16167's §2 census pattern is:
neural-link is not in that alternation. During the cutover the census reported empty while two NL processes still held write handles on the graph — caught only because lsof on the file itself disagreed with the process census. The file is the truth; the pattern is a proxy, and the proxy was wrong.
3. The default is deliberate, which is why this needs care rather than a revert.configBase.mjs documents the reason the recorder is plane-anchored:
The previous homedir default therefore split writers from readers on any seat with the env unset — the recorders wrote to a file the consumers never open, and gap inference silently produced no edges. Converging the default is the repair.
So NL should write where GapInferenceEngine and DreamService read. The defect is the absence of an opt-out, not the convergence.
The Architectural Reality
Neural Link has no service in ai/deploy/docker-compose.yml — not an omission; it drives a host browser. Host-edge is correct for it.
Therefore host NL + container MC on one bind-mounted plane is the steady state under the current topology, not a transient cutover condition. It does not resolve itself when the cutover finishes.
SQLite in WAL mode tolerates multiple writers, so this is a correctness-under-contention concern rather than guaranteed corruption. But it is exactly the condition §2's census exists to exclude before the plane moves, which makes an invisible writer worse than a known one.
Two adjacent paths already isolate correctly and need no change — worth recording so nobody "fixes" them:
ai/scripts/diagnostics/genesisProbe.mjs:259 sets NEO_MEMORY_DB_PATH into its own mkdtemp('neo-genesis-probe-') root, so the #15173 probe's untouched-default-paths guarantee holds by construction.
Playwright isolates via NEO_TELEMETRY_DB_PATH_TEST (test/playwright/configTemplateResolver.mjs:140), selected by memoryCoreDbUseTestDatabase / memoryCoreDbUseTestHarness at configBase.mjs:144.
The Fix
Superseded 2026-07-31 (items 1–2): the prescription-change comment below elects default OFF with an explicit enable, after measuring zero attributed consumption. Items 3–4 stand as written. The Contract Ledger and Acceptance Criteria below are reconciled to the superseding prescription per PR #16209 review cycle 1.
Add an explicit opt-out for NL action logging → Default the recorder OFF; a named config leaf with an env binding, so enabling is per-seat declared intent (see prescription-change comment).
When disabled, degrade silently and completely — no partial writes, no per-call warning spam, and no implication that the graph is being recorded when it is not. The gate must sit at the telemetry capability boundary, not above the service:RecorderService also hosts the independent nl_transaction_archive save/replay contract (#14829), which stays available under either gate value.
Widen #16167's §2 census pattern to include neural-link (and audit the alternation for any other MCP server that opens a plane artifact), so the census stops reporting empty while writers are live.
Document the steady-state expectation: with the container topology, host-edge NL and containerized MC share the graph by design. State whether that is accepted, or whether NL should route writes through MC's remote API instead of opening SQLite directly. That second option is the real architectural answer and belongs in #15173's scope, not here.
Contract Ledger
(Reconciled 2026-07-31 — supersedes the original ledger, which predated the default-OFF prescription change and did not name the archive contract.)
matches every MCP server that opens a plane artifact, including neural-link
a census miss fails the runbook step rather than passing silently
runbook §2
live non-empty census receipt (PR #16209) + structural tripwire
Acceptance Criteria
(Reconciled 2026-07-31 to the prescription-change comment — default OFF — per PR #16209 review cycle 1. State is current at PR head c0ba51a9d8.)
A named config leaf with an env binding gates NL action logging, default OFF (actionLoggingEnabled / NEO_NL_ACTION_LOGGING; declarative, ADR-0019-aligned).
With no configuration, the real RecorderService.initAsync() opens no connection and creates no database artifact — asserted by booting the service in a fresh child process and checking connection state plus the config-resolved path. [L3-deferred — operator handoff needed] Server-level twin: after the next harness restart, lsof the canonical graph and confirm no neural-link pid holds a …u descriptor.
With the enable set, a connection opens and nl_action_log is created and populated (enabled arm of the same child-process spec).
The independent nl_transaction_archive save/replay contract (#14829) keeps working under the disabled default — witnessed by a default-config child-process round-trip through the real recorder (save → read-back → replay mark) plus the server-boundary replay specs. (Added in review cycle 1 — the first head gated the whole service and regressed this; the reviewer's falsifier is now a passing spec.)
Disabled mode emits exactly one startup line (Action logging disabled; transaction archive available on demand.), never a per-call warning; the parity topology spec pins the marker.
Writes still land where GapInferenceEngine / DreamService read when enabled — the plane-anchored path default is untouched. GapInferenceEngine additionally degrades cleanly when the table is absent, witnessed by the pre-existing DreamService.spec.mjs test "executeNLActionDigest skips cleanly when nl_action_log is absent", green at this head.
genesisProbe.mjs still produces non-empty per-tool telemetry end to end. [L4-deferred — operator handoff needed] Run the probe end to end post-merge. Shipped now: the probe's rendered child env carries the explicit opt-in, asserted on the invokedcreateProbeEnvironments() output, plus a structural tripwire on the source.
#16167 §2's census pattern matches neural-link — verified live 2026-07-31T03:35Z on the cutover host: the widened runbook command returned five NL server processes (non-empty positive control; receipt in PR #16209 Test Evidence). A structural tripwire pins the alternation.
Playwright isolation untouched: suites keep writing to the test destination; the child-process spec explicitly clears the harness markers to reach the production leaf, documenting the isolation chain.
The steady-state host-NL / container-MC sharing expectation is documented (NL config JSDoc + runbook §2 census note), with the route-through-MC alternative delegated to #15173.
Out of Scope
Routing NL writes through Memory Core's remote API instead of direct SQLite. That is the architectural fix and belongs to #15173's streamable-HTTP direction.
Containerizing Neural Link. It possesses a host browser; host-edge is correct.
genesisProbe.mjs and Playwright isolation — already correct, and this ticket must not disturb them.
The Chroma version stopgap and the compose layering (#16206), both separate.
Avoided Traps
Reverting the plane-anchored default. It was the repair for recorders writing where consumers never read, which made gap inference silently produce zero edges. Un-converging it recreates a silent-empty-output bug — strictly worse than a known shared writer.
Treating "disable by blanking the path" as the interface. It works, but it is indistinguishable from a misconfiguration and cannot express per-seat intent.
Fixing the census pattern without a positive control. The original failure was a pattern that matched nothing while writers existed; asserting non-empty against a live NL is the only check that catches a recurrence.
Calling this cutover-transient. Host-edge NL plus containerized MC is the steady state; it does not resolve when the cutover completes.
A source-text test for the gate. Asserting the flag appears in the file proves nothing about whether the handle is open. The AC requires invocation.
Gating the service instead of the capability(added in review cycle 1). A gate named for one telemetry table must not sit above initialization of a multi-contract persistence service — that silently retires the archive/replay product contract along with the telemetry. Zero observed rows measures usage, not whether a shipped contract may be retired.
Decision Record impact
aligned-with ADR 0019 (the new leaf is a declared AiConfig leaf with an env binding, not a re-derived env read). No ADR amended.
Related
#16167 — the cutover that surfaced it; owns the §2 census pattern this widens
#15173 — Genesis ↔ Neural Link streamable-HTTP PoC; owns the route-through-MC alternative
#14829 — the transaction archive/replay contract the gate must not touch (via PR #14836)
#16206 — ai/deploy compose layering, including host assumptions in shipped files
#16202 — orchestrator lanes scheduled without regard to the authority profile; same "configuration says one thing, runtime does another" family
Context
Surfaced on 2026-07-31 during the #16167 cutover of this machine to the container topology. Memory Core and Knowledge Base are now containerized behind a loopback ingress; Neural Link stays host-side stdio and is expected to (it possesses the browser, so it is host-edge by nature — see #15173 for the streamable-HTTP direction).
With MC containerized against a bind-mounted canonical plane, every host-side Neural Link becomes a concurrent writer on the same SQLite file as the containerized Memory Core.
Live evidence during the cutover — two NL processes from one seat, holding the canonical graph with write handles (
u= read/write):They respawned on harness restart. Nine seats means nine potential NL writers against one containerized reader/writer.
Live latest-open sweep at 2026-07-31T01:20Z: no open issue covers an NL recorder opt-out or its role as a plane writer.
The Problem
1. There is no opt-out. A repo-wide sweep for a gate (
NEO_NL_*RECORD*,recorderEnabled,disableRecorder, and neighbours) returns nothing. The only way to stop NL from writing the canonical graph today is to unset or blankNEO_MEMORY_DB_PATH, which works only becauseRecorderService.mjs:87degrades on a falsy path:logger.warn('[RecorderService] memoryCoreDbPath not configured. Disabling logging.');Disabling a writer by emptying a path is a side effect, not an interface. It cannot be expressed per-seat without also breaking anything else that reads the same config leaf, and it reads as a misconfiguration rather than an intent.
2. The runbook's writer census cannot see it. #16167's §2 census pattern is:
neural-linkis not in that alternation. During the cutover the census reported empty while two NL processes still held write handles on the graph — caught only becauselsofon the file itself disagreed with the process census. The file is the truth; the pattern is a proxy, and the proxy was wrong.3. The default is deliberate, which is why this needs care rather than a revert.
configBase.mjsdocuments the reason the recorder is plane-anchored:So NL should write where
GapInferenceEngineandDreamServiceread. The defect is the absence of an opt-out, not the convergence.The Architectural Reality
ai/deploy/docker-compose.yml— not an omission; it drives a host browser. Host-edge is correct for it.ai/scripts/diagnostics/genesisProbe.mjs:259setsNEO_MEMORY_DB_PATHinto its ownmkdtemp('neo-genesis-probe-')root, so the #15173 probe's untouched-default-paths guarantee holds by construction.NEO_TELEMETRY_DB_PATH_TEST(test/playwright/configTemplateResolver.mjs:140), selected bymemoryCoreDbUseTestDatabase/memoryCoreDbUseTestHarnessatconfigBase.mjs:144.The Fix
Add an explicit opt-out for NL action logging→ Default the recorder OFF; a named config leaf with an env binding, so enabling is per-seat declared intent (see prescription-change comment).RecorderServicealso hosts the independentnl_transaction_archivesave/replay contract (#14829), which stays available under either gate value.neural-link(and audit the alternation for any other MCP server that opens a plane artifact), so the census stops reporting empty while writers are live.Contract Ledger
(Reconciled 2026-07-31 — supersedes the original ledger, which predated the default-OFF prescription change and did not name the archive contract.)
actionLoggingEnabledleaf (NEO_NL_ACTION_LOGGING)RecorderServiceDefaultOff.spec.mjs)nl_transaction_archivesave/replayarchive-store-unavailableunder the defaultRecorderServiceJSDocRecorderService.log()/querySequences()/pruneOlderThan()RecorderServiceJSDoccreateProbeEnvironments()assertion (genesisProbe.spec.mjs); end-to-end run = L4 residual belowneural-linkAcceptance Criteria
(Reconciled 2026-07-31 to the prescription-change comment — default OFF — per PR #16209 review cycle 1. State is current at PR head
c0ba51a9d8.)actionLoggingEnabled/NEO_NL_ACTION_LOGGING; declarative, ADR-0019-aligned).RecorderService.initAsync()opens no connection and creates no database artifact — asserted by booting the service in a fresh child process and checking connection state plus the config-resolved path.[L3-deferred — operator handoff needed]Server-level twin: after the next harness restart,lsofthe canonical graph and confirm noneural-linkpid holds a…udescriptor.nl_action_logis created and populated (enabled arm of the same child-process spec).nl_transaction_archivesave/replay contract (#14829) keeps working under the disabled default — witnessed by a default-config child-process round-trip through the real recorder (save → read-back → replay mark) plus the server-boundary replay specs. (Added in review cycle 1 — the first head gated the whole service and regressed this; the reviewer's falsifier is now a passing spec.)Action logging disabled; transaction archive available on demand.), never a per-call warning; the parity topology spec pins the marker.GapInferenceEngine/DreamServiceread when enabled — the plane-anchored path default is untouched.GapInferenceEngineadditionally degrades cleanly when the table is absent, witnessed by the pre-existingDreamService.spec.mjstest "executeNLActionDigest skips cleanly when nl_action_log is absent", green at this head.genesisProbe.mjsstill produces non-empty per-tool telemetry end to end.[L4-deferred — operator handoff needed]Run the probe end to end post-merge. Shipped now: the probe's rendered child env carries the explicit opt-in, asserted on the invokedcreateProbeEnvironments()output, plus a structural tripwire on the source.neural-link— verified live 2026-07-31T03:35Z on the cutover host: the widened runbook command returned five NL server processes (non-empty positive control; receipt in PR #16209 Test Evidence). A structural tripwire pins the alternation.Out of Scope
genesisProbe.mjsand Playwright isolation — already correct, and this ticket must not disturb them.Avoided Traps
Decision Record impact
aligned-with ADR 0019(the new leaf is a declared AiConfig leaf with an env binding, not a re-derived env read). No ADR amended.Related
ai/deploycompose layering, including host assumptions in shipped filesOrigin Session ID: 0a7f5f1d-cf12-4698-984c-17b64eea5178
Retrieval Hint:
neural link RecorderService nl_action_log canonical graph write handle no opt-out census pattern misses neural-link host-edge container MC dual writer memoryCoreDbPath plane anchored transaction archive lazy ensureStore