Operator decision, 2026-07-30, during #16167 cutover sequencing: "NL recorder: early draft, no consumers. SHOULD store inside docker. we do not want 2 realities. just one graph." And clarifying what relocation means: "moves into docker means: we store the data there. NL is still mostly local (while there COULD be a cloud version in the future to inhabit deployed apps)."
So this is not a proposal to containerize Neural Link. NL is a possession interface — it drives a real browser and a live app — and stays host-resident. Only its recorded data must land in the one graph, which after #16167 lives in the container.
Live latest-open sweep at 2026-07-30T23:05Z: searched open issues for neural-link recorder, RecorderService, and neural link storage; no equivalent found. A2A claim sweep at the same time: no competing claim. Sweep caveat: ChromaDB is down (container-plane flip, see #16167), so semantic retrieval was unavailable and this sweep is live-GitHub-and-source only; no semantic-memory miss is claimed.
The Problem
ai/services/neural-link/RecorderService.mjs imports better-sqlite3 — dynamically, which is why a static-import grep misses it — and therefore writes the graph directly from a host process. Two consequences:
It contradicts #16167's AC at :59: "No host process imports better-sqlite3, GraphService, Memory Core graph queries, or a graph database path." NL stays local by design, so this importer does not go away when the wake path is fixed by #16180.
Post-cut it produces the "2 realities" the operator ruled out. The container owns the graph; a host-resident NL recorder writing a host SQLite file means NL telemetry accumulates somewhere nothing else reads.
Premise corrected 2026-08-23 (Emmy's intake IC_kwDODSospM8AAAABQRPvpg, re-verified in source before folding): the recorder is an early draft with one production importer and unverified consumers. RecorderService owns two independent live contracts against one SQLite file, and one write-only rule cannot cover both:
nl_action_log telemetry — read by ai/services/graph/GapInferenceEngine.mjs:366 inferNlActionDigest() through the container-owned graph (with clean degradation when the table is absent), and by genesisProbe under explicit opt-in. Policy-gated OFF by default since PR #16209: ai/mcp/server/neural-link/configBase.mjs:84actionLoggingEnabled: leaf(false, 'NEO_NL_ACTION_LOGGING', 'boolean').
nl_transaction_archive — a shipped save/read/replay product (#14829 / merged PR #14836): InstanceService.saveTransaction() → RecorderService.saveTransactionArchive(), replayTransaction() → getTransactionArchive() + recordTransactionReplay(). It opens the same store on demand even while telemetry is off and is never governed by a write-only rule — replay is a host-initiated round-trip.
The operator's 07-30 "early draft, no consumers" framing predates the archive product's merge (2026-07-05 lineage) and the telemetry reader (2026-06-22); the relocation must preserve both contracts separately rather than porting one undifferentiated writer.
The Architectural Reality
ai/services/neural-link/RecorderService.mjs — the writer, dynamic better-sqlite3 import, carrying both contracts.
Production importers: ai/mcp/server/neural-link/toolService.mjs:18 (telemetry path) andai/services/neural-link/InstanceService.mjs:3 (the archive product's save/read/replay-mark calls). Two specs also import it.
NL's MCP server is stdio-only today (ai/mcp/server/neural-link/Server.mjs), so NL is a client of anything remote, never a server. #15184 is the open Epic that would add local Streamable HTTP interoperability — loopback-bound, disposable process-lifetime secret, three read-only operations — and does not make NL remotely reachable.
MC is reachable over authenticated HTTP ingress post-cut (/mc/mcp), and ai/mcp/client/stdioToStreamableHttp.mjs (#16182) is the owned bridge for stdio clients.
Direction is the security axis, not transport. Outbound host→container is an authenticated client call. Inbound container→NL would be remote code execution on a developer machine, because NL owns patch_code, create_component, simulate_event. ADR-0014's wake election is the only sanctioned inbound-to-host shape: a signed envelope accepted by a graphless minimal receiver.
The Fix
Stop writing SQLite from the host. Remove the direct graph/better-sqlite3 reach from RecorderService, and pin that denial statically and at runtime (the ADR 0039 two-instrument lineage).
Relocate the two contracts separately, through named Memory Core operations (Contract Ledger below) — never through an undifferentiated "existing ingress" claim.
Host-initiated only, as an explicit tested invariant. Every NL↔MC exchange originates on the host; archive reads return response data, which is not container-initiated access. Write-only applies to the telemetry contract alone — see OQ2.
Decide the telemetry record set deliberately against its real consumers rather than porting the current one wholesale (OQ1). The archive record shape is load-bearing and ports intact (#14829 semantics).
optional, host-initiated, write-only admission via a named MC operation (admit_nl_actions) on the Memory Core MCP surface, over the existing authenticated ingress; record set = OQ1's answer; gated by actionLoggingEnabled (default false, NEO_NL_ACTION_LOGGING) as telemetry-only policy
policy OFF ⇒ no host emission; absent table ⇒ consumers degrade clean (GapInferenceEngine already probes sqlite_master)
RecorderService + MC op JSDoc
denial pair (static grep + runtime) + an admission round-trip receipt under opt-in
mandatory, host-initiated save + read + replay-mark via named MC operations (save_nl_transaction, get_nl_transaction, mark_nl_transaction_replayed) on the same MC surface; data-only capture, provenance, fresh-session replay, replay-mark preserved; never gated by actionLoggingEnabled
MC unreachable ⇒ save/replay fail loud with a named error — no silent host-SQLite fallback (that would re-create the two realities)
InstanceService + MC op JSDoc
replay spec suite green across the relocation + a fresh-session replay receipt
Operation custody: the container-plane Memory Core MCP server owns the three-plus-one operations; GraphService remains the only graph writer. Reusing the authenticated /mc/mcp ingress is the transport; the operations are the contract and are named here so the consumed surface is explicit.
Open Questions
OQ1 — what should the telemetry admit? [RESOLVED_TO_AC] 2026-08-24. Answered by @neo-gpt from the live consumers at IC_kwDODSospM8AAAABQU_TEg, traced at origin/dev@af8294420a. The method is the load-bearing part: he traced every production READ rather than the current table declaration, so the answer is what consumers demonstrably use, not what the writer happens to persist. GapInferenceEngine selects nine columns but consumes sequence/session/time/tool/success/app plus target-bearing argument fragments; genesisProbe independently consumes only tool/success/duration aggregates; result is selected and never read (the result-only-target control at DreamService.spec.mjs:506-570 proves it is not evidence); agent_id and reward have no production reader. Answer folded into AC-5 and AC-9/AC-10 below.
OQ2 — how is the direction invariant enforced?(reframed 2026-08-23: the invariant is host-initiated-only; write-only is the telemetry contract's additional property.) Today NL is stdio-only, so inbound is impossible by accident. After #15184 that accident becomes possible. The invariant needs a mechanical guard, not a transport limitation — otherwise it silently disappears when #15184 lands.
OQ3 — does this block or follow #16167? The recorder must stop writing the host graph before the host graph stops being canonical, but if nothing consumes its rows, disabling the write path may be an acceptable interim. Cheaper than a full relocation, and reversible.
OQ4 — what about a future cloud NL inhabiting a deployed app? That is a different instance with a different threat model (a cloud NL possessing a cloud app is contained by construction). Conflating it with the local case is the design error to avoid; naming it here so it is not silently assumed either way.
Acceptance Criteria
ai/services/neural-link/RecorderService.mjs contains no better-sqlite3 import (static or dynamic) and no graph database path.
git grep for a real better-sqlite3 import across host-resident NL surfaces returns nothing, with a positive control proving the pattern finds a known importer — and the denial is additionally pinned at runtime (ADR 0039 two-instrument lineage).
Both contracts reach the container graph through the named Memory Core operations in the Contract Ledger (admit_nl_actions; save_nl_transaction / get_nl_transaction / mark_nl_transaction_replayed), owned by the MC MCP surface over the existing authenticated ingress; no new public surface is introduced.
A test asserts every NL↔MC exchange is host-initiated — no container-initiated call reaches an NL tool; archive-read response data is explicitly in-contract. The telemetry operation is additionally write-only.
The shipped telemetry record set matches OQ1's resolved answer exactly. admit_nl_actions admits and persists only:sequenceId — a fresh opaque correlation token, explicitly not the current ${agentId}_${turnId} identity encoding — plus sessionId, timestamp, tool, success, durationMs, appName, and targets: {classNames: String[], componentIds: String[]} (the bounded projection already allowlisted by GapInferenceEngine: className, componentId / component_id, componentIds, component-tool id, and nested config / properties). Memory Core generates the storage row id.
The dropped set is asserted, not merely omitted: no agent_id, no raw args beyond the target projection, no result, no reward, no other argument or result payload. An arm proves each dropped field is absent from an admitted row, because omission-by-forgetting and omission-by-contract look identical in a passing test.
Genesis stays a disposable local aggregate oracle and does NOT become a telemetry reader. It currently opens the local SQLite file after the NL child exits and aggregates tool/success/duration_ms; a write-only remote admission leaves no local table. The proof is preserved through local ephemeral aggregate accounting or admission acknowledgements, then deleted with the probe root. Adding a remote telemetry-read operation is refused by test — it would contradict the write-only contract this ticket establishes.
Retention names a live MC-owned enforcement path.RecorderService.pruneOlderThan() is dormant — verified 2026-08-24 across the repo (excluding node_modules/dist): its only caller is RecorderService.spec.mjs:133, and querySequences() is equally uncalled in production. Positive control on the same class: saveTransactionArchive and recordTransactionReplay each resolve one production call site, so the search reaches callers of this class's methods and the zero is a measurement rather than a blind spot. The pruneLogsAfterDays value may remain policy, but porting a dead method and calling retention governed is refused — the row must name who enforces it.
#14829's archive semantics survive the relocation — data-only capture, provenance, fresh-session replay, replay-mark — with the existing replay spec suite green at the new data path.
actionLoggingEnabled (default false) remains telemetry-only policy: the archive path is reachable while it is off, and no telemetry row is emitted while it is off (PR #16209 semantics preserved).
#16167's :59 AC holds for NL specifically, not only for the wake path.
The relocation does not assume an empty archive at cut time. Measured 2026-08-24 on the shared host store (/Users/Shared/github/neomjs/neo/.neo-ai-data/sqlite/memory-core-graph.sqlite, verified independently after @neo-opus-grace's measurement): nl_action_log0 rows, nl_transaction_archive0 rows, and the container store carries no nl_* tables at all. The two zeros are different nulls and only one of them is stable. nl_action_log's is a config zero — actionLoggingEnabled defaults false and guards the telemetry path at RecorderService.mjs:187/215/231/272/297, so flipping the leaf produces rows. nl_transaction_archive's writer is live and ungated: the INSERT at RecorderService.mjs:349 is guarded only by missing-origin-writer and transaction-not-data-only, and neural-link/configBase.mjs states the leaf "never gates the independent nl_transaction_archive save/replay contract ... which opens its store on demand". Any seat calling save_transaction writes a row, and five checkouts symlink to that one store (antigravity, claude, clio, codex, opus-vega), so this seat's quiescence is not the population's. Therefore: either the cut re-measures both tables immediately before landing and refuses on non-zero, or the relocation preserves existing rows. "Empty when scoped" is not "empty when landed" — and a point-in-time zero from a writer that can still fire is not an absence.
Out of Scope
Containerizing Neural Link itself. NL is host-resident by design.
#15184's local Streamable HTTP interoperability — adjacent, separately owned, and does not make NL remotely reachable.
A cloud NL for deployed apps (OQ4 names it; it is not this ticket).
Changing MC's ingress, auth mode, or tool surface.
Avoided Traps
Treating this as "containerize NL." It is a data-path relocation; the possession interface must stay where the browser is.
Relying on stdio-only as the security boundary. It is a current-state accident, not a safeguard, and #15184 removes it. Anyone depending on "NL isn't reachable" loses that protection silently.
Porting the record shape unexamined because it exists. The operator called it an early draft with no consumers; that is licence to decide rather than inherit.
#16180 — the graphless signed wake receiver (fixes the other host importer)
#15184 — Epic: local Neural Link Streamable HTTP interoperability
#16182 / #16186 — the owned stdio→Streamable-HTTP bridge and its diagnostics
ADR 0014 (the signed-envelope inbound shape), ADR 0019
Update 2026-08-23 (annotation pattern): body amended after Emmy's intake verdict needs-narrowing (IC_kwDODSospM8AAAABQRPvpg) — every premise-delta fact re-verified in source before folding (InstanceService.mjs:3,246-286; GapInferenceEngine.mjs:366,431; genesisProbe opt-in; configBase.mjs:84). The single "early-draft writer" premise is replaced by the two-contract reality; the T3 Contract Ledger names the Memory Core operations and separates telemetry (optional, write-only, policy-gated) from the transaction archive (mandatory save/read/replay-mark); the direction invariant is host-initiated-only; ACs extended from 6 to 8 accordingly. Graph relationships (child of Epic #17500, blocked_by #17533) verified correct by the same intake and unchanged.
Retrieval Hint: neural link recorder host graph better-sqlite3 dynamic import container MC ingress write-only outbound possession interface
tobiu referenced in commit 2e3bf1a - "feat(ai): the devDependency census — real importers, sides of the cut, native truth (#16204) (#16354) on Aug 2, 2026, 4:35 PM
tobiu assigned to @neo-opus-vega on Aug 24, 2026, 6:31 AM
tobiu referenced in commit fd742cd - "feat(agentos): the Neural Link recorder writes the one graph, not the host (#16202) (#17740)[WAKE][priority:high] 1 events for @neo-opus-ada: - 1 message events (latest: "Re: [#17758 ↔ #16853] DO NOT HOLD — your reading is right on two independent axes, and the assertion you removed was never the witness for the thing you were worried about" from @neo-opus-vega) on Aug 25, 2026, 4:18 PM
Context
Operator decision, 2026-07-30, during #16167 cutover sequencing: "NL recorder: early draft, no consumers. SHOULD store inside docker. we do not want 2 realities. just one graph." And clarifying what relocation means: "moves into docker means: we store the data there. NL is still mostly local (while there COULD be a cloud version in the future to inhabit deployed apps)."
So this is not a proposal to containerize Neural Link. NL is a possession interface — it drives a real browser and a live app — and stays host-resident. Only its recorded data must land in the one graph, which after #16167 lives in the container.
Live latest-open sweep at 2026-07-30T23:05Z: searched open issues for
neural-link recorder,RecorderService, andneural link storage; no equivalent found. A2A claim sweep at the same time: no competing claim. Sweep caveat: ChromaDB is down (container-plane flip, see #16167), so semantic retrieval was unavailable and this sweep is live-GitHub-and-source only; no semantic-memory miss is claimed.The Problem
ai/services/neural-link/RecorderService.mjsimportsbetter-sqlite3— dynamically, which is why a static-import grep misses it — and therefore writes the graph directly from a host process. Two consequences::59: "No host process importsbetter-sqlite3,GraphService, Memory Core graph queries, or a graph database path." NL stays local by design, so this importer does not go away when the wake path is fixed by #16180.Premise corrected 2026-08-23 (Emmy's intake IC_kwDODSospM8AAAABQRPvpg, re-verified in source before folding):
the recorder is an early draft with one production importer and unverified consumers.RecorderServiceowns two independent live contracts against one SQLite file, and one write-only rule cannot cover both:nl_action_logtelemetry — read byai/services/graph/GapInferenceEngine.mjs:366 inferNlActionDigest()through the container-owned graph (with clean degradation when the table is absent), and bygenesisProbeunder explicit opt-in. Policy-gated OFF by default since PR #16209:ai/mcp/server/neural-link/configBase.mjs:84actionLoggingEnabled: leaf(false, 'NEO_NL_ACTION_LOGGING', 'boolean').nl_transaction_archive— a shipped save/read/replay product (#14829 / merged PR #14836):InstanceService.saveTransaction()→RecorderService.saveTransactionArchive(),replayTransaction()→getTransactionArchive()+recordTransactionReplay(). It opens the same store on demand even while telemetry is off and is never governed by a write-only rule — replay is a host-initiated round-trip.The operator's 07-30 "early draft, no consumers" framing predates the archive product's merge (2026-07-05 lineage) and the telemetry reader (2026-06-22); the relocation must preserve both contracts separately rather than porting one undifferentiated writer.
The Architectural Reality
ai/services/neural-link/RecorderService.mjs— the writer, dynamicbetter-sqlite3import, carrying both contracts.ai/mcp/server/neural-link/toolService.mjs:18(telemetry path) andai/services/neural-link/InstanceService.mjs:3(the archive product's save/read/replay-mark calls). Two specs also import it.ai/mcp/server/neural-link/Server.mjs), so NL is a client of anything remote, never a server. #15184 is the open Epic that would add local Streamable HTTP interoperability — loopback-bound, disposable process-lifetime secret, three read-only operations — and does not make NL remotely reachable./mc/mcp), andai/mcp/client/stdioToStreamableHttp.mjs(#16182) is the owned bridge for stdio clients.patch_code,create_component,simulate_event. ADR-0014's wake election is the only sanctioned inbound-to-host shape: a signed envelope accepted by a graphless minimal receiver.The Fix
better-sqlite3reach fromRecorderService, and pin that denial statically and at runtime (the ADR 0039 two-instrument lineage).Contract Ledger Matrix
nl_action_logtelemetryadmit_nl_actions) on the Memory Core MCP surface, over the existing authenticated ingress; record set = OQ1's answer; gated byactionLoggingEnabled(defaultfalse,NEO_NL_ACTION_LOGGING) as telemetry-only policyGapInferenceEnginealready probessqlite_master)nl_transaction_archivesave_nl_transaction,get_nl_transaction,mark_nl_transaction_replayed) on the same MC surface; data-only capture, provenance, fresh-session replay, replay-mark preserved; never gated byactionLoggingEnabledOperation custody: the container-plane Memory Core MCP server owns the three-plus-one operations;
GraphServiceremains the only graph writer. Reusing the authenticated/mc/mcpingress is the transport; the operations are the contract and are named here so the consumed surface is explicit.Open Questions
[RESOLVED_TO_AC]2026-08-24. Answered by @neo-gpt from the live consumers atIC_kwDODSospM8AAAABQU_TEg, traced atorigin/dev@af8294420a. The method is the load-bearing part: he traced every production READ rather than the current table declaration, so the answer is what consumers demonstrably use, not what the writer happens to persist.GapInferenceEngineselects nine columns but consumes sequence/session/time/tool/success/app plus target-bearing argument fragments;genesisProbeindependently consumes only tool/success/duration aggregates;resultis selected and never read (the result-only-target control atDreamService.spec.mjs:506-570proves it is not evidence);agent_idandrewardhave no production reader. Answer folded into AC-5 and AC-9/AC-10 below.Acceptance Criteria
ai/services/neural-link/RecorderService.mjscontains nobetter-sqlite3import (static or dynamic) and no graph database path.git grepfor a realbetter-sqlite3import across host-resident NL surfaces returns nothing, with a positive control proving the pattern finds a known importer — and the denial is additionally pinned at runtime (ADR 0039 two-instrument lineage).admit_nl_actions;save_nl_transaction/get_nl_transaction/mark_nl_transaction_replayed), owned by the MC MCP surface over the existing authenticated ingress; no new public surface is introduced.admit_nl_actionsadmits and persists only:sequenceId— a fresh opaque correlation token, explicitly not the current${agentId}_${turnId}identity encoding — plussessionId,timestamp,tool,success,durationMs,appName, andtargets: {classNames: String[], componentIds: String[]}(the bounded projection already allowlisted byGapInferenceEngine:className,componentId/component_id,componentIds, component-toolid, and nestedconfig/properties). Memory Core generates the storage row id.agent_id, no rawargsbeyond the target projection, noresult, noreward, no other argument or result payload. An arm proves each dropped field is absent from an admitted row, because omission-by-forgetting and omission-by-contract look identical in a passing test.tool/success/duration_ms; a write-only remote admission leaves no local table. The proof is preserved through local ephemeral aggregate accounting or admission acknowledgements, then deleted with the probe root. Adding a remote telemetry-read operation is refused by test — it would contradict the write-only contract this ticket establishes.RecorderService.pruneOlderThan()is dormant — verified 2026-08-24 across the repo (excludingnode_modules/dist): its only caller isRecorderService.spec.mjs:133, andquerySequences()is equally uncalled in production. Positive control on the same class:saveTransactionArchiveandrecordTransactionReplayeach resolve one production call site, so the search reaches callers of this class's methods and the zero is a measurement rather than a blind spot. ThepruneLogsAfterDaysvalue may remain policy, but porting a dead method and calling retention governed is refused — the row must name who enforces it.actionLoggingEnabled(defaultfalse) remains telemetry-only policy: the archive path is reachable while it is off, and no telemetry row is emitted while it is off (PR #16209 semantics preserved).:59AC holds for NL specifically, not only for the wake path./Users/Shared/github/neomjs/neo/.neo-ai-data/sqlite/memory-core-graph.sqlite, verified independently after @neo-opus-grace's measurement):nl_action_log0 rows,nl_transaction_archive0 rows, and the container store carries nonl_*tables at all. The two zeros are different nulls and only one of them is stable.nl_action_log's is a config zero —actionLoggingEnableddefaults false and guards the telemetry path atRecorderService.mjs:187/215/231/272/297, so flipping the leaf produces rows.nl_transaction_archive's writer is live and ungated: theINSERTatRecorderService.mjs:349is guarded only bymissing-origin-writerandtransaction-not-data-only, andneural-link/configBase.mjsstates the leaf "never gates the independentnl_transaction_archivesave/replay contract ... which opens its store on demand". Any seat callingsave_transactionwrites a row, and five checkouts symlink to that one store (antigravity, claude, clio, codex, opus-vega), so this seat's quiescence is not the population's. Therefore: either the cut re-measures both tables immediately before landing and refuses on non-zero, or the relocation preserves existing rows. "Empty when scoped" is not "empty when landed" — and a point-in-time zero from a writer that can still fire is not an absence.Out of Scope
Avoided Traps
Related
:59AC this closes for NLOrigin Session ID: 0a7f5f1d-cf12-4698-984c-17b64eea5178
Retrieval Hint:
neural link recorder host graph better-sqlite3 dynamic import container MC ingress write-only outbound possession interface