---
id: 12597
title: Memory Core lifecycle singletons leak across specs under --workers=1
state: CLOSED
labels:
  - bug
  - ai
  - testing
assignees:
  - neo-gpt
createdAt: '2026-06-05T20:57:11Z'
updatedAt: '2026-06-05T23:21:51Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12597'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-06-05T23:21:51Z'
---
# Memory Core lifecycle singletons leak across specs under --workers=1

## 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.mjs` — `cleanupChromaManager()` 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.mjs` — `beforeAll` 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"

## Timeline

- 2026-06-05T20:57:13Z @neo-opus-ada added the `bug` label
- 2026-06-05T20:57:13Z @neo-opus-ada added the `ai` label
- 2026-06-05T20:57:13Z @neo-opus-ada added the `testing` label
- 2026-06-05T20:59:13Z @neo-opus-ada cross-referenced by PR #12598
- 2026-06-05T21:19:45Z @neo-gpt assigned to @neo-gpt
- 2026-06-05T21:37:00Z @neo-gpt cross-referenced by PR #12600
- 2026-06-05T23:02:32Z @neo-opus-vega cross-referenced by PR #12604
- 2026-06-05T23:13:03Z @neo-claude-opus cross-referenced by #12331
- 2026-06-05T23:21:51Z @tobiu referenced in commit `7ecbdfb` - "test(memory): reset lifecycle bindings between specs (#12597) (#12600)"
- 2026-06-05T23:21:51Z @tobiu closed this issue
- 2026-06-05T23:26:16Z @neo-claude-opus cross-referenced by #12180
- 2026-06-05T23:26:32Z @neo-claude-opus cross-referenced by #12143
- 2026-06-06T00:35:06Z @neo-opus-vega cross-referenced by PR #12608

