Context
Observed in a downstream consumer project that pins neo as a git dependency and runs whitebox E2E through our Playwright fixture. Its CI began failing with every test passing and then the process aborting:
Statement::~Statement() [node_modules/better-sqlite3/build/Release/better_sqlite3.node]
Aborted (core dumped)
17 passed immediately above, then exit code 1. Three open merge requests in that project were red on this and nothing else. It is intermittent — the same dependency tree went green on other runs the same day — which is the signature of a teardown race rather than a test failure.
That consumer does not use SQLite. It has zero first-party importers of better-sqlite3; the package appears exactly once in its repository, as a devDependencies line.
Observation vs inference. Observed: the abort trace, the zero-importer count, the two experiments in The Fix below. Inferred: the trigger described in The Architectural Reality, which is a code-read and is not fully pinned — see the open question there.
The Problem
test/playwright/fixtures.mjs needs six Neural Link symbols. It obtains them from ai/services.mjs, which is a barrel over the entire Brain — knowledge-base, memory-core, ingestion, graph. Importing six Neural Link services therefore pulls memory-core and the graph layer into the test process of every consumer that uses our fixture.
The cost is not abstract:
- A native binding is loaded that the consumer never uses, and its destructor runs against a torn-down environment at process exit. That abort is what reds the pipeline, after every test has already passed — so the failure points at nothing the author changed.
- The dependency becomes non-removable downstream. A consumer that correctly identifies
better-sqlite3 as unused cannot drop it, because our fixture demands it.
- Install weight. Consumers carry native SQLite (and the graph surface behind it) purely to run browser tests.
The Neural Link services themselves are clean. Their imports are src/core/Base.mjs, their own config/logger, RuntimeFreshnessService, their siblings, and node builtins plus ws/fs-extra. Nothing in the Neural Link surface needs the Brain. The coupling is entirely an artifact of the barrel.
The Architectural Reality
test/playwright/fixtures.mjs:3-11 — the six symbols are destructured from '../../ai/services.mjs'.
ai/services.mjs:16-18, 203-209 — the barrel re-exports NeuralLink_* alongside every other service family.
ai/services/memory-core/GraphService.mjs:6 — statically imports ../../../ai/graph/storage/SQLite.mjs, so the barrel reaches the graph storage module at load.
ai/graph/storage/SQLite.mjs:49 — loads the native binding through a dynamic import, carrying this comment:
// Use dynamic imports to prevent native Node module evaluation crashes inside browser/test runtimes
That mitigation already exists and already names this exact hazard. The barrel path defeats it in practice.
ai/services/neural-link/ — the six services live in their own modules and can be imported directly.
Open question, deliberately not asserted: a dynamic import should not fire at module load, yet removing the package makes the fixture fail to load with ERR_MODULE_NOT_FOUND … imported from ai/graph/storage/SQLite.mjs. Something on the barrel path reaches the dynamic import during initialization. Identifying that caller is part of the work, and it may reveal a second defect worth its own ticket — an eagerly-opened graph connection would be a problem beyond the test harness.
The Fix
Import the Neural Link services directly in the fixture instead of through the barrel:
import Neo from '../../src/Neo.mjs';
import * as core from '../../src/core/_export.mjs';
import NeuralLink_ConnectionService from '../../ai/services/neural-link/ConnectionService.mjs';
The two core imports are load-bearing and are the non-obvious half. The barrel was also bootstrapping the Neo global as an undeclared side effect; direct imports alone fail with ReferenceError: Neo is not defined at src/core/Compare.mjs:166, thrown far from the import that caused it. ai/mcp/server/neural-link/run-bridge.mjs:2-3 already does exactly this pair, so the shape has precedent.
Both halves verified empirically against a consumer's suite:
| experiment |
result |
remove better-sqlite3, keep the barrel import |
ERR_MODULE_NOT_FOUND from ai/graph/storage/SQLite.mjs — fixture will not load |
remove better-sqlite3, use direct imports + core bootstrap |
17/17 passed |
The absent dependency is the assertion: the suite passing without it installed is what proves the barrel was the only thing requiring it.
Acceptance Criteria
Out of Scope
- Restructuring
ai/services.mjs itself. The barrel is legitimate for callers that genuinely want the whole Brain; the defect is that a test fixture uses it for six symbols. Narrowing the consumer is the proportionate fix.
- The MCP bridge's own import graph —
run-bridge.mjs is already lean and unaffected.
- Whether
chromadb can also be dropped by consumers. It is reached by three real importers under ai/services/**, so it is a genuine dependency of the barrel and a separate question once the fixture stops pulling that surface in.
- Changing the dynamic-import mitigation at
SQLite.mjs:49. It is correct; this ticket stops the path that renders it moot.
Avoided Traps
- Bumping
better-sqlite3 to make the abort go away. The teardown crash is a real symptom, and a version bump might well silence it — but it would leave every consumer loading a native database binding to run browser tests. Treating the symptom here also costs the install-weight and non-removability wins.
- Telling the consumer to drop the dependency. Attempted first, and it fails: the fixture will not load without it. A consumer cannot fix this from its own repository, which is precisely what makes it ours.
- Assuming a lazy import is inert.
SQLite.mjs:49 is a deliberate dynamic import with a comment explaining that it exists to avoid native-module crashes in test runtimes — and the crash happened anyway. A mitigation is only as good as the paths that respect it; the presence of the guard is not evidence the hazard is handled.
- Scoping a dependency search to the directory you believe owns it. While diagnosing this, a search of
ai/services/** for better-sqlite3 returned zero — with a positive control (chromadb, three hits) that passed. The control validated the matcher, not the path scope; the real importer was one directory over in ai/graph/. For "what does X transitively need", scope to the whole package.
Related
#135-class install-weight concerns in consuming projects · #17357 (container startup facts) is unrelated but touches the same ai/ tree · the RuntimeFreshnessService dependency of the Neural Link services is unaffected by this change.
Decision Record impact: none — this narrows one import site; it does not alter the service topology or any ADR-governed boundary.
Origin Session ID: 1baae1f2-97e4-418c-9119-c3112763f552
Handoff Retrieval Hints
query_raw_memories: "playwright fixture barrel import better-sqlite3 teardown abort"
query_raw_memories: "Neo is not defined Compare.mjs direct service import bootstrap"
- Anchors:
test/playwright/fixtures.mjs:3-11 · ai/services/memory-core/GraphService.mjs:6 · ai/graph/storage/SQLite.mjs:49 · ai/mcp/server/neural-link/run-bridge.mjs:2-3
Gate records — live latest-open sweep: latest 20 open issues checked 2026-08-18, no equivalent found. A2A in-flight sweep: latest 30 messages, all read-states; no overlapping [lane-claim]. Agent OS Structure Map gate: run; owning folders ai/services/neural-link and ai/graph/storage both exist as siblings — no new file, no placement decision.
Context
Observed in a downstream consumer project that pins neo as a git dependency and runs whitebox E2E through our Playwright fixture. Its CI began failing with every test passing and then the process aborting:
17 passedimmediately above, thenexit code 1. Three open merge requests in that project were red on this and nothing else. It is intermittent — the same dependency tree went green on other runs the same day — which is the signature of a teardown race rather than a test failure.That consumer does not use SQLite. It has zero first-party importers of
better-sqlite3; the package appears exactly once in its repository, as adevDependenciesline.Observation vs inference. Observed: the abort trace, the zero-importer count, the two experiments in The Fix below. Inferred: the trigger described in The Architectural Reality, which is a code-read and is not fully pinned — see the open question there.
The Problem
test/playwright/fixtures.mjsneeds six Neural Link symbols. It obtains them fromai/services.mjs, which is a barrel over the entire Brain — knowledge-base, memory-core, ingestion, graph. Importing six Neural Link services therefore pulls memory-core and the graph layer into the test process of every consumer that uses our fixture.The cost is not abstract:
better-sqlite3as unused cannot drop it, because our fixture demands it.The Neural Link services themselves are clean. Their imports are
src/core/Base.mjs, their own config/logger,RuntimeFreshnessService, their siblings, and node builtins plusws/fs-extra. Nothing in the Neural Link surface needs the Brain. The coupling is entirely an artifact of the barrel.The Architectural Reality
test/playwright/fixtures.mjs:3-11— the six symbols are destructured from'../../ai/services.mjs'.ai/services.mjs:16-18, 203-209— the barrel re-exportsNeuralLink_*alongside every other service family.ai/services/memory-core/GraphService.mjs:6— statically imports../../../ai/graph/storage/SQLite.mjs, so the barrel reaches the graph storage module at load.ai/graph/storage/SQLite.mjs:49— loads the native binding through a dynamic import, carrying this comment:That mitigation already exists and already names this exact hazard. The barrel path defeats it in practice.
ai/services/neural-link/— the six services live in their own modules and can be imported directly.Open question, deliberately not asserted: a dynamic import should not fire at module load, yet removing the package makes the fixture fail to load with
ERR_MODULE_NOT_FOUND … imported from ai/graph/storage/SQLite.mjs. Something on the barrel path reaches the dynamic import during initialization. Identifying that caller is part of the work, and it may reveal a second defect worth its own ticket — an eagerly-opened graph connection would be a problem beyond the test harness.The Fix
Import the Neural Link services directly in the fixture instead of through the barrel:
import Neo from '../../src/Neo.mjs'; import * as core from '../../src/core/_export.mjs'; import NeuralLink_ConnectionService from '../../ai/services/neural-link/ConnectionService.mjs'; // …InstanceService, ComponentService, DataService, DockService, RuntimeService, InteractionServiceThe two core imports are load-bearing and are the non-obvious half. The barrel was also bootstrapping the
Neoglobal as an undeclared side effect; direct imports alone fail withReferenceError: Neo is not defined at src/core/Compare.mjs:166, thrown far from the import that caused it.ai/mcp/server/neural-link/run-bridge.mjs:2-3already does exactly this pair, so the shape has precedent.Both halves verified empirically against a consumer's suite:
better-sqlite3, keep the barrel importERR_MODULE_NOT_FOUNDfromai/graph/storage/SQLite.mjs— fixture will not loadbetter-sqlite3, use direct imports + core bootstrapThe absent dependency is the assertion: the suite passing without it installed is what proves the barrel was the only thing requiring it.
Acceptance Criteria
test/playwright/fixtures.mjsimports its Neural Link services fromai/services/neural-link/*rather than fromai/services.mjs.Neoglobal bootstrap is imported explicitly, and a comment records why — the next reader must not "simplify" it away, because its absence fails as aReferenceErrorin an unrelated file.better-sqlite3. Verifiable without a consumer: no module underai/graph/**orai/services/memory-core/**is reachable from the fixture's import graph.SQLite.mjs:49's dynamic import during initialization is identified and named in the PR, or a follow-up ticket is filed if it is a separate defect.Out of Scope
ai/services.mjsitself. The barrel is legitimate for callers that genuinely want the whole Brain; the defect is that a test fixture uses it for six symbols. Narrowing the consumer is the proportionate fix.run-bridge.mjsis already lean and unaffected.chromadbcan also be dropped by consumers. It is reached by three real importers underai/services/**, so it is a genuine dependency of the barrel and a separate question once the fixture stops pulling that surface in.SQLite.mjs:49. It is correct; this ticket stops the path that renders it moot.Avoided Traps
better-sqlite3to make the abort go away. The teardown crash is a real symptom, and a version bump might well silence it — but it would leave every consumer loading a native database binding to run browser tests. Treating the symptom here also costs the install-weight and non-removability wins.SQLite.mjs:49is a deliberate dynamic import with a comment explaining that it exists to avoid native-module crashes in test runtimes — and the crash happened anyway. A mitigation is only as good as the paths that respect it; the presence of the guard is not evidence the hazard is handled.ai/services/**forbetter-sqlite3returned zero — with a positive control (chromadb, three hits) that passed. The control validated the matcher, not the path scope; the real importer was one directory over inai/graph/. For "what does X transitively need", scope to the whole package.Related
#135-class install-weight concerns in consuming projects ·#17357(container startup facts) is unrelated but touches the sameai/tree · theRuntimeFreshnessServicedependency of the Neural Link services is unaffected by this change.Decision Record impact:
none— this narrows one import site; it does not alter the service topology or any ADR-governed boundary.Origin Session ID: 1baae1f2-97e4-418c-9119-c3112763f552
Handoff Retrieval Hints
query_raw_memories: "playwright fixture barrel import better-sqlite3 teardown abort"query_raw_memories: "Neo is not defined Compare.mjs direct service import bootstrap"test/playwright/fixtures.mjs:3-11·ai/services/memory-core/GraphService.mjs:6·ai/graph/storage/SQLite.mjs:49·ai/mcp/server/neural-link/run-bridge.mjs:2-3Gate records — live latest-open sweep: latest 20 open issues checked 2026-08-18, no equivalent found. A2A in-flight sweep: latest 30 messages, all read-states; no overlapping
[lane-claim]. Agent OS Structure Map gate: run; owning foldersai/services/neural-linkandai/graph/storageboth exist as siblings — no new file, no placement decision.