LearnNewsExamplesServices
Frontmatter
id12597
titleMemory Core lifecycle singletons leak across specs under --workers=1
stateClosed
labels
bugaitesting
assigneesneo-gpt
createdAtJun 5, 2026, 10:57 PM
updatedAtJun 6, 2026, 1:21 AM
githubUrlhttps://github.com/neomjs/neo/issues/12597
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 6, 2026, 1:21 AM

Memory Core lifecycle singletons leak across specs under --workers=1

neo-opus-ada
neo-opus-ada commented on Jun 5, 2026, 10:57 PM

Context

Surfaced while completing #12435 (aiConfig snapshot/restore isolation). With the config-leaf restore in place, a full-affected-set --workers=1 run still has one cross-spec failure that config-restore does not fix: QueryReRanker.spec "StorageRouter re-ranker should handle valid queries without crashing" fails because its seeded memories are not found (result.ids[0].length is 0). The same spec passes alone (7/7).

Proven pre-existing: checking out the pre-#12435 (HEAD~1) versions of the same specs and re-running the identical --workers=1 group produces the identical failure (57 passed / 1 failed). So this is orthogonal to — and not introduced by — the aiConfig config-leaf bleed #12435 addresses.

The Problem

The Memory Core lifecycle services are process-singletons. Memory_LifecycleService._initPromise, Memory_ChromaManager.client, and the ChromaManager collection caches bind to whichever spec inits the lifecycle first in a --workers=1 worker process. Specs guard init with if (!_initPromise) initAsync(); else ready();, so a later spec calling ready() inherits the first spec's collection binding.

The divergence that fails the test: Memory_Service.addMemory (the seed) writes through the bound ChromaManager (first spec's collection), while StorageRouter.getMemoryCollection() resolves the collection name from the (now config-restored) aiConfig at call time (the later spec's name). Seed and query land in different collections → 0 results.

The asymmetry that lets the stale binding survive: cleanupChromaManager() resets _initPromise = null plus the ChromaManager collection caches, but TestLifecycleHelper.cleanupGraphService() (the graph-only cleanup used by graph specs) does not. A spec whose afterAll uses the graph-only cleanup leaves the lifecycle bound for the next spec in the worker.

The Architectural Reality

  • test/playwright/unit/ai/services/memory-core/util.mjscleanupChromaManager() resets _initPromise + ChromaManager caches; TestLifecycleHelper.cleanupGraphService() clears the graph db only (no _initPromise / ChromaManager-cache reset).
  • test/playwright/unit/ai/services/memory-core/QueryReRanker.spec.mjsbeforeAll boots the lifecycle via the if (!_initPromise) … else ready() short-circuit, seeds 3 memories via Memory_Service.addMemory, then the failing test queries via StorageRouter.getMemoryCollection().
  • Distinct from #12331 / #12180 / #12143, which target orphan test-* collection accumulation / store-persistence cleanup across runs. This is an in-memory singleton-binding bleed within a single --workers=1 run — a test-correctness issue, not store persistence.

The Fix (candidate prescriptions)

  • Unify spec cleanup so every memory-core / graph spec resets the lifecycle singleton in afterAll — e.g. have cleanupGraphService also null _initPromise + the ChromaManager caches, OR introduce a shared resetMemoryCoreLifecycle() that both cleanup paths call.
  • OR drop the else ready() short-circuit so each describe re-inits unconditionally (heavier per-spec cost; weigh during implementation).

Prefer the unified-reset-helper approach (single source of truth invoked by both cleanup paths).

Acceptance Criteria

  • A full-affected-set --workers=1 run (the 13 #12435 specs) is green — QueryReRanker's StorageRouter seed→query passes in the group, not only alone.
  • Both cleanup paths (chroma + graph-only) reset the lifecycle-singleton binding, OR an equivalent per-spec re-init guarantees a fresh binding.
  • Regression guard: each spec still passes alone (no over-reset that breaks single-spec runs).

Out of Scope

  • Orphan test-* collection accumulation / store purge (#12331, #12180, #12143).
  • aiConfig config-leaf snapshot/restore (#12435 — shipped).
  • Redesigning the lifecycle-singleton pattern itself.

Related

  • Surfaced while completing #12435. The AC3 (--workers=1 green) residual for that ticket lives here.
  • Sibling test-isolation cluster: #12331 (unified test-store isolation), #12180, #12143. May fold into #12331's unified-isolation scope — maintainer/operator triage.

Decision Record impact

none.

Live latest-open sweep: checked latest 20 open issues at 2026-06-05T20:55Z; nearest neighbours (#12331/#12180/#12143) target store-accumulation, not in-memory singleton binding — no equivalent found.

Origin Session ID: b457a732-8cec-4fac-8ace-bcb977e1b076 Retrieval Hint: "Memory Core lifecycle singleton _initPromise binding bleed --workers=1 QueryReRanker StorageRouter seed query cleanupGraphService reset"

tobiu referenced in commit 7ecbdfb - "test(memory): reset lifecycle bindings between specs (#12597) (#12600)" on Jun 6, 2026, 1:21 AM
tobiu closed this issue on Jun 6, 2026, 1:21 AM