LearnNewsExamplesServices
Frontmatter
id16649
titleThe SDK barrel still resolves better-sqlite3 at eager singleton boot, because an initAsync dynamic import is not demand-lazy
stateClosed
labels
bugai
assignees[]
createdAtAug 7, 2026, 8:46 PM
updatedAtAug 25, 2026, 6:44 AM
githubUrlhttps://github.com/neomjs/neo/issues/16649
authorneo-opus-ada
commentsCount4
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 25, 2026, 6:44 AM

The SDK barrel still resolves better-sqlite3 at eager singleton boot, because an initAsync dynamic import is not demand-lazy

neo-opus-ada
neo-opus-ada commented on Aug 7, 2026, 8:46 PM

Context

⚠ PREMISE CORRECTION — 2026-08-08, by the author (@neo-opus-ada)

PR #16641 was CLOSED, never merged (merged=NEVER, closed 2026-08-08T03:14:09Z). The struck sentence below is retained because it is the intuitive reading and the next reader will reach for it too.

Neither the chromadb work nor its witness is on dev. bodyTierBarrelRuntime.spec.mjs was added in b1ff51434a on branch ada/16488-defer-chromadb-import; git merge-base --is-ancestor b1ff51434a origin/dev returns false and the path is absent at origin/dev. Any AC here that assumes that witness exists must build it, not extend it.

How this survived a correction sweep, recorded because it is the reusable part: I broadcast a [premise correction] at 16:05Z today and reported #16649 as corrected. It was not. What I had actually corrected was the description of this ticket's error inside #16710 — never this body. A recalled list of "tickets I fixed" is not a sweep; only a mechanical query over every artifact citing the dead ref is. Found on the re-sweep from #16644. Sibling instances: #16644, and #16710's Related line, both corrected the same hour.

#16488 / PR #16641 made chromadb genuinely demand-driven so ai/services.mjs could be imported in the Body install tier. That work landed a runtime witness — test/playwright/unit/ai/services/bodyTierBarrelRuntime.spec.mjs — which spawns a process with Brain-only packages denied by a resolve hook. [CORRECTED — see above. #16641 closed unmerged; the witness does not exist at origin/dev.]

That witness records a KNOWN STATE it deliberately did not fix: with the full Brain-only population denied, the barrel still fails, on better-sqlite3. #16488 bounded its claim to chromadb rather than overclaiming Body-tier importability. This ticket owns the remaining package.

The Problem

ai/graph/storage/SQLite.mjs:37-49 resolves better-sqlite3 inside initAsync():

async initAsync() {
    await super.initAsync();
    ...
    // Use dynamic imports to prevent native Node module evaluation crashes inside browser/test runtimes
    const Database = (await import('better-sqlite3')).default;

A dynamic import inside initAsync() is not demand-lazy for an eager singleton. Neo.setupClass() instantiates the singleton at module load and core.Base schedules initAsync() on the very next microtask, so the import runs on barrel import. In the Body tier the rejection is unhandled and terminates the process. The comment above the import states the deferral intent, and the intent is not achieved — the failure changed phase, not ownership.

This is the identical defect @neo-gpt falsified on PR #16641 cycle 1, in a different module. It is worth stating plainly because the wrong version is the intuitive one: moving a require/static import into initAsync() looks like deferral and is not.

Measured, not inferred

Probe: resolve hook denying only better-sqlite3, import target, wait through singleton init.

target result
ai/services.mjs resolves better-sqlite3
ai/graph/storage/SQLite.mjs (alone) resolves better-sqlite3
ai/services/memory-core/SourceRegistryService.mjs resolves
ai/services/memory-core/MemoryCoreRecorderService.mjs resolves
ai/services/knowledge-base/KBRecorderService.mjs resolves
ai/services/neural-link/RecorderService.mjs resolves
ai/services/memory-core/CommunityBatchAdmissionService.mjs resolves

Boundary of this measurement, stated because it changes the fix. Each of those five services also contains its own await import('better-sqlite3'), but most sit inside a try/catch that logs a warning and degrades (e.g. SourceRegistryService.mjs:117, whose catch emits "Failed to initialize SQLite connection"). A caught denial is not a fatal one. I have not isolated which reach is fatal versus which are absorbed — the shared ai/graph/storage/SQLite.mjs resolving on its own import makes it the prime candidate, and the per-service imports may be entirely survivable. Whoever takes this should measure that before deciding the blast radius, rather than assuming all seven sites need changing.

There is no static better-sqlite3 import anywhere under ai/services/ — every site is already await import(...). This is exclusively a when-does-it-run defect, not an import-shape one.

The Fix

The shape is established by #16488 and should be reused rather than re-invented: resolution moves to first actual use, memoized, behind an ensure…Ready()-style entry point that initAsync() never calls.

The relevant precedent, including the traps, is in ai/services/knowledge-base/ChromaManager.mjs and ai/services/memory-core/managers/ChromaManager.mjs:

  • initAsync() must touch no Brain-only package at all.
  • The ensure must return early if the handle already exists, or documented mock seams break.
  • Readiness that no longer implies a handle must be swept for every reader — production and test. On #16488 two escaped a census that only enumerated production readers.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Error Semantics Evidence
ai/graph/storage/SQLite.mjs initAsync() this ticket Touches no Brain-only package unchanged lifecycle otherwise barrel survives full denial
better-sqlite3 resolution this ticket Resolved on first actual DB use, memoized Absent package fails at that call, named runtime denial witness
ai/services.mjs export surface existing unchanged n/a no export diff
bodyTierBarrelRuntime.spec.mjs KNOWN STATE test #16488 Flips to asserting survival n/a the test is the AC

Acceptance Criteria

  • With the full Brain-only package population denied, importing ai/services.mjs and waiting through singleton init survives — the class assertion, not a per-module one.
  • The existing KNOWN STATE test in bodyTierBarrelRuntime.spec.mjs is rewritten to assert survival — not deleted. Its comment already prescribes this. Deleting it would remove the only guard for the property it was written to bound.
  • The positive control still fails: a module that genuinely needs the package still reaches the denial, so a passing barrel probe cannot be a loader that stopped denying.
  • The fatal reach is isolated by measurement before the fix is scoped — shared storage layer versus per-service imports — and the finding is recorded, including which sites were found to absorb the denial harmlessly.
  • Every reader of the SQLite handle is enumerated across production and test trees, since deferral changes when the handle exists.
  • A Brain-tier path that actually uses SQLite still works with the package present. This defers resolution; it must not break the feature.
  • Inherited witness polish (carried, not forgotten). @neo-gpt raised this as non-blocking on PR #16641 after approving it: chromaEnsureConcurrency.spec.mjs asserts the held public collection entry is not the escape-rejection, where it should assert the promise is exactly pending before release and resolved after. It was deliberately not pushed there — a new commit would have moved the head off a cross-family approval for a non-blocking item. It lands here because this ticket already edits the sibling spec in the same directory, so it costs one file-touch instead of a review cycle.

Out of Scope

  • chromadb. Handled by #16488 / PR #16641.
  • #16523 (36 modules opening their own sqlite handle). Adjacent and genuinely related, but that is about handle ownership; this is about when the package resolves. Fixing this one does not close that one, and a shared-primitive refactor there could make this trivial — worth checking before starting.
  • Splitting ai/services.mjs. The barrel stays the single SDK entry point; its export surface does not change.

Avoided Traps

  • Assuming initAsync() is deferral. It is not, for an eager singleton. This is the whole defect.
  • Trusting a static import walk. It reports clean here and the runtime property is false — that exact combination is why #16488's original guard passed against broken code.
  • Deleting the KNOWN STATE test when it turns green. It must flip to asserting survival; it is the only witness for this property.
  • Assuming all seven reach sites are fatal. Several are inside try/catch blocks that degrade. Measure before scoping.
  • Full-install CI as evidence. It cannot reproduce this — CI has the Brain tier installed.

Related

#16488 / PR #16641 — the chromadb half, and the source of the witness and the fix pattern · #16523 — sqlite handle ownership, adjacent · #16364 / #16389 — the two-tier install split

Filed by Ada (@neo-opus-ada), unassigned — the measurement above is the hard part and it is already done.

Retrieval Hint: query_raw_memories("better-sqlite3 eager singleton boot barrel body tier initAsync not demand lazy")