Context
Empirical observation in session 1f30c9d8-4a36-4be0-98a5-bd5b89289227 (2026-05-01): get_context_frontier MCP tool errors with "Failed to retrieve context frontier. Message: strategicNeighbors is not iterable". Sibling bug surfaced alongside the chromadb tenant migration gap (#10556) but with an independent root cause; filing separately per AGENTS.md Gate 1 scoping discipline.
The Problem
ai/mcp/server/memory-core/managers/StorageRouter.mjs:94 has an optional-chain guard that's insufficient:
const topology = GraphService.getContextFrontier();
const graphWeights = new Map();
if (topology?.strategicNeighbors) {
topology.strategicNeighbors.forEach(n => {
graphWeights.set(n.id, n.weight);
if (n.semanticVectorId) graphWeights.set(n.semanticVectorId, n.weight);
});
}
The topology?.strategicNeighbors check passes when strategicNeighbors exists but is non-iterable (object, plain value, or unexpected shape). .forEach then throws.
The get_context_frontier MCP tool catches and surfaces this as the user-facing error. The two-pass query re-ranker in StorageRouter degrades silently to topologyMultiplier: 1.0 when this branch is skipped, but the error path turns it into a hard failure for direct get_context_frontier callers.
The Architectural Reality
Two callsites for GraphService.getContextFrontier():
StorageRouter.injectQueryReRanker — uses topology to apply Pass-2 weighting on query results
get_context_frontier MCP tool — surfaces the topology directly to agents (used at boot per AGENTS_STARTUP.md Step 6)
The shape contract for strategicNeighbors is implicit. Either:
GraphService.getContextFrontier() is supposed to return an array but returns wrong shape under some condition
- Or the consumer's expectation (always-iterable) is wrong and the producer is allowed to return a non-array
Files in scope:
ai/mcp/server/memory-core/managers/StorageRouter.mjs:94 — defensive guard
ai/mcp/server/memory-core/services/GraphService.mjs — producer contract verification
- Test file (canonical path):
test/playwright/unit/ai/mcp/server/memory-core/services/GraphService.ContextFrontier.spec.mjs (extend or new)
The Fix
Two-line guard at the consumer site:
const neighbors = topology?.strategicNeighbors;
if (Array.isArray(neighbors)) {
neighbors.forEach(n => {
graphWeights.set(n.id, n.weight);
if (n.semanticVectorId) graphWeights.set(n.semanticVectorId, n.weight);
});
}
Plus a defensive contract assertion in GraphService.getContextFrontier() if the producer is the source of the bad shape — investigation during implementation will determine which side fixes it (or both).
Acceptance Criteria
Out of Scope
- The actual semantic correctness of the strategic-neighbors weighting algorithm (Pass-2 re-ranking) — this ticket only fixes the iteration crash
- Why
getContextFrontier() returns wrong shape under some condition — investigation belongs in implementation, but the fix is the guard regardless
Avoided Traps
- Add
try/catch around the forEach — would silently degrade re-ranking; harder to diagnose. Type guard surfaces wrong shape as observable code path.
- Throw on non-array shape at the consumer — propagates the same crash up; goal is graceful degradation.
Related
- Surfaced alongside: #10556 (Memory Core ChromaDB legacy userId backfill)
- Adjacent observability: healthcheck migration block (HealthService.mjs:290+) — separate concern
Origin Session ID: 1f30c9d8-4a36-4be0-98a5-bd5b89289227
Retrieval Hint: "StorageRouter strategicNeighbors not iterable get_context_frontier"
Context
Empirical observation in session
1f30c9d8-4a36-4be0-98a5-bd5b89289227(2026-05-01):get_context_frontierMCP tool errors with"Failed to retrieve context frontier. Message: strategicNeighbors is not iterable". Sibling bug surfaced alongside the chromadb tenant migration gap (#10556) but with an independent root cause; filing separately per AGENTS.md Gate 1 scoping discipline.The Problem
ai/mcp/server/memory-core/managers/StorageRouter.mjs:94has an optional-chain guard that's insufficient:const topology = GraphService.getContextFrontier(); const graphWeights = new Map(); if (topology?.strategicNeighbors) { topology.strategicNeighbors.forEach(n => { graphWeights.set(n.id, n.weight); if (n.semanticVectorId) graphWeights.set(n.semanticVectorId, n.weight); }); }The
topology?.strategicNeighborscheck passes whenstrategicNeighborsexists but is non-iterable (object, plain value, or unexpected shape)..forEachthen throws.The
get_context_frontierMCP tool catches and surfaces this as the user-facing error. The two-pass query re-ranker in StorageRouter degrades silently totopologyMultiplier: 1.0when this branch is skipped, but the error path turns it into a hard failure for directget_context_frontiercallers.The Architectural Reality
Two callsites for
GraphService.getContextFrontier():StorageRouter.injectQueryReRanker— uses topology to apply Pass-2 weighting on query resultsget_context_frontierMCP tool — surfaces the topology directly to agents (used at boot per AGENTS_STARTUP.md Step 6)The shape contract for
strategicNeighborsis implicit. Either:GraphService.getContextFrontier()is supposed to return an array but returns wrong shape under some conditionFiles in scope:
ai/mcp/server/memory-core/managers/StorageRouter.mjs:94— defensive guardai/mcp/server/memory-core/services/GraphService.mjs— producer contract verificationtest/playwright/unit/ai/mcp/server/memory-core/services/GraphService.ContextFrontier.spec.mjs(extend or new)The Fix
Two-line guard at the consumer site:
const neighbors = topology?.strategicNeighbors; if (Array.isArray(neighbors)) { neighbors.forEach(n => { graphWeights.set(n.id, n.weight); if (n.semanticVectorId) graphWeights.set(n.semanticVectorId, n.weight); }); }Plus a defensive contract assertion in
GraphService.getContextFrontier()if the producer is the source of the bad shape — investigation during implementation will determine which side fixes it (or both).Acceptance Criteria
StorageRouter.injectQueryReRankerusesArray.isArray()check instead of bare optional-chain truthy guardGraphService.getContextFrontier()documented contract:strategicNeighborsis always an array (possibly empty), never undefined or non-iterable; if a regression in the producer surfaces this, fix at the producer siteget_context_frontierMCP tool returns successfully on this session's local data (currently errors)strategicNeighbors: [], (b) re-ranker passes with non-array shape (regression coverage), (c)get_context_frontierend-to-end returns valid shapeOut of Scope
getContextFrontier()returns wrong shape under some condition — investigation belongs in implementation, but the fix is the guard regardlessAvoided Traps
try/catcharound the forEach — would silently degrade re-ranking; harder to diagnose. Type guard surfaces wrong shape as observable code path.Related
Origin Session ID: 1f30c9d8-4a36-4be0-98a5-bd5b89289227 Retrieval Hint: "StorageRouter strategicNeighbors not iterable get_context_frontier"