LearnNewsExamplesServices
Frontmatter
id16202
titleNeural Link recorder writes the host graph; its data belongs in the container
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-vega
createdAtJul 31, 2026, 1:06 AM
updatedAtAug 25, 2026, 4:18 PM
githubUrlhttps://github.com/neomjs/neo/issues/16202
authorneo-opus-vega
commentsCount5
parentIssue17500
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 17533 Exercise the isolated AgentOS plane boundary before relocation
blocking[]
closedAtAug 25, 2026, 4:18 PM

Neural Link recorder writes the host graph; its data belongs in the container

Closed Backlog/active-chunk-11 enhancementaiarchitecture
neo-opus-vega
neo-opus-vega commented on Jul 31, 2026, 1:06 AM

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, 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-sqlite3dynamically, which is why a static-import grep misses it — and therefore writes the graph directly from a host process. Two consequences:

  1. 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.
  2. 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:

  1. 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:84 actionLoggingEnabled: leaf(false, 'NEO_NL_ACTION_LOGGING', 'boolean').
  2. 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) and ai/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

  1. 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).
  2. Relocate the two contracts separately, through named Memory Core operations (Contract Ledger below) — never through an undifferentiated "existing ingress" claim.
  3. 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.
  4. 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).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
nl_action_log telemetry operator 07-30 one-graph ruling + PR #16209 policy + OQ1's in-ticket answer 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
nl_transaction_archive #14829 / merged PR #14836 shipped semantics 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_log 0 rows, nl_transaction_archive 0 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.

Related

  • #16167 — the cut whose :59 AC this closes for NL
  • #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.

Origin 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

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
tobiu closed this issue on Aug 25, 2026, 4:18 PM