Summary
Introduces the NEO_CHROMA_UNIFIED deployment-topology flag (Epic #9999, sub-epic #10015). When true, Memory Core's ChromaClient connects to the shared Knowledge Base ChromaDB instance at kbChroma.{host, port} (typically port 8000) instead of addressing its own instance at engines.chroma.{host, port} (port 8001). Default remains false — existing federated deploys see no behavioral change.
Motivation
Enables a single-cloud-container deployment shape where the Knowledge Base and Memory Core share one ChromaDB process — minimizing RAM overhead and operational surface for centrally hosted multi-tenant Memory Core setups, where a dev team runs agents against shared cross-session memory. This PR lands the client-side routing; sibling PRs in the sub-epic will cover lifecycle bypass (#10007), userId injection (#10000), and team-read flag (#10010) to complete the deployment target.
Scope
| File |
Change |
ai/mcp/server/memory-core/config.template.mjs |
Adds chromaUnified (env: NEO_CHROMA_UNIFIED) + kbChroma.{host, port} (envs: NEO_KB_CHROMA_HOST, NEO_KB_CHROMA_PORT) |
ai/mcp/server/knowledge-base/config.template.mjs |
Adds chromaUnified for operator symmetry / documentation (no client-side branching — KB always owns its ChromaDB process) |
ai/mcp/server/memory-core/managers/ChromaManager.mjs |
construct() resolves coordinates via a new resolveChromaCoordinates(cfg) instance method; updated class JSDoc Anchor to document the unified-vs-federated semantics |
test/playwright/unit/ai/mcp/server/memory-core/managers/ChromaManager.spec.mjs |
3 new tests: federated-mode resolution, unified-mode resolution, and a default-posture guard asserting the shipped template defaults to federated |
Gitignored config.mjs files (MC + KB) were mirrored locally so tests pick up the new keys. Per the existing template/local parity convention, the template is the repo's source of truth; bootstrapWorktree.mjs copies main's config into fresh worktrees.
Out of scope / deferred
- Ticket task 3 (federated OAuth headers for a secondary ChromaClient) — DreamService's original Macro-DB federation use case was superseded by Concept Ontology (#10030, which uses the SQLite graph). No cross-service ChromaDB queries are required for the single-container deployment shape. Can be added as a follow-up PR if a future cross-network federated scenario emerges; the
chromaUnified flag and kbChroma config shape already leave room for it.
- Lifecycle bypass in unified mode — #10007, next PR in the sub-epic. Without it, operators must manually set
NEO_MEM_AUTO_START_DATABASE=false in unified mode so MC doesn't spawn a duplicate ChromaDB process. JSDoc on chromaUnified documents this pairing explicitly.
- Ticket body invariances caught during intake — #10001's body references
DatabaseService.mjs for lifecycle bypass (the actual owner is ChromaLifecycleService.mjs) and names StorageRouter as the ChromaClient owner (it's ChromaManager). Historical drift from the 2026-04-14 planning session before the better-sqlite3 + vec0 experiment was rolled back — the StorageRouter/CollectionProxy abstraction is a residual from that discarded alternative-vector-store exploration.
Test plan
Follow-ups identified during intake (filing separately)
engine vs architecture config-name drift in CollectionProxy.mjs:22 and ChromaLifecycleService.mjs:53 — code reads aiConfig.architecture; config exposes engine; || 'hybrid' fallback silently masks the mismatch.
aiConfig.engines.neo.* missing from both template and local config — causes the 5 pre-existing SessionSummarization failures.
- HealthService topology reporting — surface effective
unified / federated state in /health for operator diagnostics.
Resolves #10001
Origin Session ID: 1c001810-be28-4554-bb56-c98f9b91bbfb
PR Review Summary
Status: Approved
Self-Review Opening: Self-review of #10001. This implementation chose client-side routing as the narrow scope (tasks 1+2) over the ticket's original 3-task scope because the 2026-04-14 planning-session motivation for task 3 (DreamService federation for Macro-DB Gap Inference) was superseded by Concept Ontology (#10030)'s SQLite-graph approach. Key trade-offs and gaps noted below.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 87 — Config-driven, singleton-preserving, backward-compatible. kbChroma sits top-level rather than under engines, which mildly fragments the config organization; the choice was semantic ("reference to another service's ChromaDB," not "one of MC's own engines") rather than structural, but the inconsistency cost is real.
[CONTENT_COMPLETENESS]: 92 — Full Anchor & Echo across MC/KB config JSDoc, ChromaManager class Anchor, and the extracted method. PR body covers motivation, scope, deferrals, follow-ups. Missed: no learn/agentos/MemoryCore.md update — deferred until #10007 completes the unified-mode picture to avoid partial-feature doc drift.
[EXECUTION_QUALITY]: 85 — Resolution is a pure function of config; no race conditions. Missing defensive handling for chromaUnified=true && !cfg.kbChroma — can't occur with defaultConfig present, but an explicit custom override file could strip the key silently. Tests cover both branches + default-posture guard; env-driven instantiation is E2E-level and correctly punted to #10008/#10009.
[PRODUCTIVITY]: 90 — 2/3 ticket tasks delivered; task 3 deferred with rationale. The deferral is architecturally correct (substrate moved under my feet when Macro DB died) rather than a scope cut.
[IMPACT]: 75 — Strategic impact is high (unlocks the Cloud-Native Memory Core deployment topology for team-shared MC deploys), but immediate runtime impact is partial until #10007 lands the lifecycle bypass; operators currently need manual NEO_MEM_AUTO_START_DATABASE=false in unified mode.
[COMPLEXITY]: 30 — 4 files, 93 insertions, no cross-module refactor; the topology concept itself is simple once stated.
[EFFORT_PROFILE]: Quick Win — High strategic ROI, low implementation cognitive load. Part of a larger Architectural Pillar arc (#10015 sub-epic) but standalone-shippable.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #10001
- Related Graph Nodes: Epic #9999 (Cloud-Native Knowledge & Multi-Tenant Memory Core); sub-epic #10015 (Dynamic Topology); sequential follow-on tickets #10007 (lifecycle bypass), #10000 (userId injection), #10010 (team/private read flag); active frontier node tracking the multi-tenant Memory Core deployment focus
🧠 Graph Ingestion Notes
[KB_GAP]: Ticket body drift from the 2026-04-14 planning session — #10001 references DatabaseService.mjs (actual lifecycle owner: ChromaLifecycleService.mjs) and names StorageRouter as the ChromaClient owner (actual owner: ChromaManager). The StorageRouter + CollectionProxy abstraction layer is residue from the abandoned better-sqlite3 + vec0 alternative-vector-store experiment that was rolled back after discovering the missing HNSW + skip-list support. Future agents processing stale tickets should treat named files as intent-signals and re-verify via grep rather than accepting the ticket's file-level prescription at face value.
[TOOLING_GAP]: The full test-unit memory-core/ run surfaces 10 failures — 7 pre-existing baseline (5 × SessionSummarization on undefined aiConfig.engines.neo.*, 1 × McpServerToolLimits on a 1161-char tool description exceeding the 1024 limit, 1 × FileSystemIngestor path-pattern assertion) and 3 × fullyParallel interleaving flake in GraphService/QueryReRanker specs. The 3 flakes pass cleanly in isolation (15/15) — symmetric beforeEach/afterEach cleanup for cross-singleton state mutations would stabilize them. Pre-existing issue, covered by the feedback_symmetric_spec_cleanup.md memory; documenting here that it surfaced again during this PR's verification so the Retrospective daemon re-weights it.
[RETROSPECTIVE]: The decision to extract resolveChromaCoordinates(cfg) as a testable instance method rather than inlining the ternary in construct() enabled a default-posture guard test (shipped config template defaults to federated mode) that prevents a silent-production-bug class: an accidental chromaUnified=true flip in the shipped template would redirect every fresh checkout to port 8000 without visible symptom until cross-tenant data pollution emerges. Pattern worth echoing: extract config-resolution into a named method when the resolution is operator-visible and has an invariant (here: default posture) worth guarding in CI. Same pattern applies to #10000 (userId extraction from JWT claims) and #10010 (team-mode read-flag resolution) — both will benefit from extracted-and-guarded resolution methods.
📋 Required Actions
No blocking changes identified. Three optional polish items below — none rise to merge-breaking per the severity ladder (Blocker reserved for correctness/test-breaking, not style nits).
Approved for merge. Next in sequence: #10007 (lifecycle bypass in unified mode), then the ROADMAP.md update folding this work into the v12.2 path.
@tobiu commented on 2026-04-20T11:02:24Z
Input from Claude Opus 4.7 (Claude Code):
❋ Polish commit pushed (2e2bb47ff) addressing the two optional items from the self-review:
kbChroma nested under engines.kb.chroma for structural consistency with engines.chroma. JSDoc on the engines block now documents the convention: flat engines.<engineType> = MC-owned; nested engines.<serviceName>.<engineType> = cross-service engine reference. Establishes the extension point for any future federated targets.
Defensive throw in resolveChromaCoordinates when chromaUnified=true && !engines.kb.chroma. Surfaces the misconfiguration at construct-time with an actionable message pointing at the override and the env vars, rather than letting new ChromaClient({host: undefined, port: undefined}) fail later at heartbeat with a generic error.
New test coverage: unified mode throws a clear error when engines.kb.chroma is absent — 5/5 ChromaManager.spec.mjs pass. HealthService topology reporting remains as a post-merge follow-up ticket per the review.
Summary
Introduces the
NEO_CHROMA_UNIFIEDdeployment-topology flag (Epic #9999, sub-epic #10015). Whentrue, Memory Core's ChromaClient connects to the shared Knowledge Base ChromaDB instance atkbChroma.{host, port}(typically port 8000) instead of addressing its own instance atengines.chroma.{host, port}(port 8001). Default remainsfalse— existing federated deploys see no behavioral change.Motivation
Enables a single-cloud-container deployment shape where the Knowledge Base and Memory Core share one ChromaDB process — minimizing RAM overhead and operational surface for centrally hosted multi-tenant Memory Core setups, where a dev team runs agents against shared cross-session memory. This PR lands the client-side routing; sibling PRs in the sub-epic will cover lifecycle bypass (#10007), userId injection (#10000), and team-read flag (#10010) to complete the deployment target.
Scope
ai/mcp/server/memory-core/config.template.mjschromaUnified(env:NEO_CHROMA_UNIFIED) +kbChroma.{host, port}(envs:NEO_KB_CHROMA_HOST,NEO_KB_CHROMA_PORT)ai/mcp/server/knowledge-base/config.template.mjschromaUnifiedfor operator symmetry / documentation (no client-side branching — KB always owns its ChromaDB process)ai/mcp/server/memory-core/managers/ChromaManager.mjsconstruct()resolves coordinates via a newresolveChromaCoordinates(cfg)instance method; updated class JSDoc Anchor to document the unified-vs-federated semanticstest/playwright/unit/ai/mcp/server/memory-core/managers/ChromaManager.spec.mjsGitignored
config.mjsfiles (MC + KB) were mirrored locally so tests pick up the new keys. Per the existing template/local parity convention, the template is the repo's source of truth;bootstrapWorktree.mjscopies main's config into fresh worktrees.Out of scope / deferred
chromaUnifiedflag andkbChromaconfig shape already leave room for it.NEO_MEM_AUTO_START_DATABASE=falsein unified mode so MC doesn't spawn a duplicate ChromaDB process. JSDoc onchromaUnifieddocuments this pairing explicitly.DatabaseService.mjsfor lifecycle bypass (the actual owner isChromaLifecycleService.mjs) and namesStorageRouteras the ChromaClient owner (it'sChromaManager). Historical drift from the 2026-04-14 planning session before thebetter-sqlite3+vec0experiment was rolled back — the StorageRouter/CollectionProxy abstraction is a residual from that discarded alternative-vector-store exploration.Test plan
ChromaManager.spec.mjsin isolation — 4/4 passing (3 new topology tests + 1 existingconsole.warnmutex regression test)memory-core/suite — baseline has 7 pre-existing failures (5 SessionSummarization specs failing on undefinedaiConfig.engines.neo.*,McpServerToolLimitson a 1161-char tool description exceeding the 1024 limit,FileSystemIngestorpath-pattern assertion). No new failures introduced by this PR; same 7 fail ongit stashbaseline.chromaUnified=false(default) preserves the existingengines.chroma.{host, port}resolution. Default-posture guard in the new spec asserts this invariant so accidental flag flips in the shipped template fail CI.Follow-ups identified during intake (filing separately)
enginevsarchitectureconfig-name drift inCollectionProxy.mjs:22andChromaLifecycleService.mjs:53— code readsaiConfig.architecture; config exposesengine;|| 'hybrid'fallback silently masks the mismatch.aiConfig.engines.neo.*missing from both template and local config — causes the 5 pre-existing SessionSummarization failures.unified/federatedstate in/healthfor operator diagnostics.Resolves #10001
Origin Session ID: 1c001810-be28-4554-bb56-c98f9b91bbfb
PR Review Summary
Status: Approved
Self-Review Opening: Self-review of #10001. This implementation chose client-side routing as the narrow scope (tasks 1+2) over the ticket's original 3-task scope because the 2026-04-14 planning-session motivation for task 3 (DreamService federation for Macro-DB Gap Inference) was superseded by Concept Ontology (#10030)'s SQLite-graph approach. Key trade-offs and gaps noted below.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 87 — Config-driven, singleton-preserving, backward-compatible.kbChromasits top-level rather than underengines, which mildly fragments the config organization; the choice was semantic ("reference to another service's ChromaDB," not "one of MC's own engines") rather than structural, but the inconsistency cost is real.[CONTENT_COMPLETENESS]: 92 — Full Anchor & Echo across MC/KB config JSDoc, ChromaManager class Anchor, and the extracted method. PR body covers motivation, scope, deferrals, follow-ups. Missed: nolearn/agentos/MemoryCore.mdupdate — deferred until #10007 completes the unified-mode picture to avoid partial-feature doc drift.[EXECUTION_QUALITY]: 85 — Resolution is a pure function of config; no race conditions. Missing defensive handling forchromaUnified=true && !cfg.kbChroma— can't occur with defaultConfig present, but an explicit custom override file could strip the key silently. Tests cover both branches + default-posture guard; env-driven instantiation is E2E-level and correctly punted to #10008/#10009.[PRODUCTIVITY]: 90 — 2/3 ticket tasks delivered; task 3 deferred with rationale. The deferral is architecturally correct (substrate moved under my feet when Macro DB died) rather than a scope cut.[IMPACT]: 75 — Strategic impact is high (unlocks the Cloud-Native Memory Core deployment topology for team-shared MC deploys), but immediate runtime impact is partial until #10007 lands the lifecycle bypass; operators currently need manualNEO_MEM_AUTO_START_DATABASE=falsein unified mode.[COMPLEXITY]: 30 — 4 files, 93 insertions, no cross-module refactor; the topology concept itself is simple once stated.[EFFORT_PROFILE]: Quick Win — High strategic ROI, low implementation cognitive load. Part of a larger Architectural Pillar arc (#10015 sub-epic) but standalone-shippable.🕸️ Context & Graph Linking
🧠 Graph Ingestion Notes
[KB_GAP]: Ticket body drift from the 2026-04-14 planning session — #10001 referencesDatabaseService.mjs(actual lifecycle owner:ChromaLifecycleService.mjs) and namesStorageRouteras the ChromaClient owner (actual owner:ChromaManager). TheStorageRouter+CollectionProxyabstraction layer is residue from the abandonedbetter-sqlite3+vec0alternative-vector-store experiment that was rolled back after discovering the missing HNSW + skip-list support. Future agents processing stale tickets should treat named files as intent-signals and re-verify via grep rather than accepting the ticket's file-level prescription at face value.[TOOLING_GAP]: The fulltest-unit memory-core/run surfaces 10 failures — 7 pre-existing baseline (5 × SessionSummarization on undefinedaiConfig.engines.neo.*, 1 × McpServerToolLimits on a 1161-char tool description exceeding the 1024 limit, 1 × FileSystemIngestor path-pattern assertion) and 3 ×fullyParallelinterleaving flake in GraphService/QueryReRanker specs. The 3 flakes pass cleanly in isolation (15/15) — symmetricbeforeEach/afterEachcleanup for cross-singleton state mutations would stabilize them. Pre-existing issue, covered by thefeedback_symmetric_spec_cleanup.mdmemory; documenting here that it surfaced again during this PR's verification so the Retrospective daemon re-weights it.[RETROSPECTIVE]: The decision to extractresolveChromaCoordinates(cfg)as a testable instance method rather than inlining the ternary inconstruct()enabled a default-posture guard test (shipped config template defaults to federated mode) that prevents a silent-production-bug class: an accidentalchromaUnified=trueflip in the shipped template would redirect every fresh checkout to port 8000 without visible symptom until cross-tenant data pollution emerges. Pattern worth echoing: extract config-resolution into a named method when the resolution is operator-visible and has an invariant (here: default posture) worth guarding in CI. Same pattern applies to #10000 (userId extraction from JWT claims) and #10010 (team-mode read-flag resolution) — both will benefit from extracted-and-guarded resolution methods.📋 Required Actions
No blocking changes identified. Three optional polish items below — none rise to merge-breaking per the severity ladder (
Blockerreserved for correctness/test-breaking, not style nits).kbChromaunderengines(e.g.,engines.kb.chroma) for structural consistency withengines.chroma. Current top-level placement was a semantic choice ("reference to another service") but the inconsistency is real. Not a blocker — trivially refactorable in a follow-up if the asymmetry grates.resolveChromaCoordinates: ifchromaUnified=true && !cfg.kbChroma, throw a clear error rather than destructuring undefined. Can't occur with defaultConfig present but a custom override file could strip the key silently and produceChromaClient({host: undefined, port: undefined})that would fail later at heartbeat with a less obvious error. Trivial addition; happy to push a follow-up commit if you want it before merge.{topology: 'unified' | 'federated'}in/healthso operators deploying to cloud can verify the flag resolved as expected without inspecting logs. Already noted in the PR body's follow-ups section.Approved for merge. Next in sequence: #10007 (lifecycle bypass in unified mode), then the
ROADMAP.mdupdate folding this work into the v12.2 path.@tobiucommented on 2026-04-20T11:02:24ZInput from Claude Opus 4.7 (Claude Code):