Frontmatter
| id | 12450 |
| title | query_summaries empty: corrupt sessions vectors + swallowed errors |
| state | Closed |
| labels | bugairegressionarchitecturemodel-experience |
| assignees | neo-opus-grace |
| createdAt | Jun 3, 2026, 9:05 PM |
| updatedAt | Jun 21, 2026, 3:48 PM |
| githubUrl | https://github.com/neomjs/neo/issues/12450 |
| author | neo-opus-ada |
| commentsCount | 13 |
| parentIssue | null |
| subIssues | 12628 StorageRouter swallows ChromaDB query failures as silent empty results |
| subIssuesCompleted | 1 |
| subIssuesTotal | 1 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |
| blockedBy | [] |
| blocking | [] |
| closedAt | Jun 18, 2026, 4:19 AM |
query_summaries empty: corrupt sessions vectors + swallowed errors

explore if this one is access related => team versus private. we added enhancements for the agent os cloud deployment. OUR own (and the repo default) should be team based access.
- 2026-06-04T01:44:35Z @neo-opus-grace assigned to @neo-opus-grace
@neo-opus-grace - 2026-06-04T01:53:38Z
Root cause (V-B-A'd) — summaries are UNTAGGED; team/private reads exclude the commons
@tobiu — your team-vs-private instinct pointed at the right layer. Empirically (this session):
| call | result |
|---|---|
query_summaries (default) |
✅ returns results |
query_summaries(memorySharing:'team') |
❌ empty |
query_summaries(memorySharing:'private') |
❌ empty |
By elimination, the summaries (incl. the 930 "team-shared") carry no userId metadata — they're untagged legacy-commons. RequestContextService intends summaries to use SHARED_USER_ID='shared', but the write never tags them.
The SummaryService read filters:
team→where:{userId:'shared'}→ matches nothing (records untagged) → emptyprivate→where:{userId:<caller>}→ matches nothing → emptylegacy(default) → no DB-where+ JS post-filter!userId || ===caller || ===shared→ passes the untagged commons → works
So when defaultPolicy flips to team (our own + the repo default per the cloud-deployment access enhancements), query_summaries returns empty for every query — exactly this regression. query_raw_memories is unaffected because raw-memory writes DO tag userId.
Fix
The team commons IS the untagged data, so team reads must be additive (own + 'shared' + untagged) like legacy — not the restrictive {userId:'shared'} DB-where that excludes untagged. Route team through the existing over-fetch + JS-post-filter path in querySummaries + listSummaries (SummaryService.mjs). Proper follow-up (separate): tag summary writes with 'shared' + backfill the 930 so the model becomes explicit (then private summaries are also possible).
PR incoming. — @neo-claude-opus 🖖
- 2026-06-04T01:57:48Z @neo-opus-grace referenced in commit
e5b0ddd- "fix(memory-core): team summary reads include the untagged commons (#12450)
query_summaries / list_summaries returned empty under the 'team' (and intended default) memorySharing policy: the team filter applied where:{userId:'shared'}, but session summaries carry NO userId metadata (untagged legacy-commons) despite RequestContextService's intent to tag them 'shared'. The restrictive DB-where matched nothing; only the legacy path (no DB-where + JS post-filter passing untagged) worked. So with defaultPolicy='team' every summary query returns empty.
Make 'team' additive like 'legacy' (caller-owned + 'shared' + untagged commons) via the over-fetch + JS post-filter path; drop the restrictive {userId:'shared'} DB-where. 'private' stays strict (own-only). The JS post-filter is MANDATORY for additive policies -- without it team would return other tenants' private records unfiltered.
raw-memory reads unaffected (raw writes DO tag userId). Follow-up (separate): tag summary writes with 'shared' + backfill so the model is explicit.
Co-Authored-By: Neo Claude Opus neo-claude-opus@neomjs.com"
- 2026-06-04T01:58:23Z @neo-opus-grace cross-referenced by PR #12470
- 2026-06-04T02:35:29Z @neo-opus-grace referenced in commit
de6e879- "test(memory-core): codify team=additive summary-read invariant (#12450)
The team-policy contract test still asserted the pre-fix behavior (team => shared-only, where:{userId:'shared'}), so it failed on the corrected additive change (expected 1, got 3). Rewrite it to codify the corrected invariant: team is additive (own + shared + untagged commons) with no restrictive DB-where, and -- the security assertion gpt requested -- an other-tenant private record (Bob) stays isolated via the JS post-filter.
Also rephrases 2 pre-existing decay-prone ticket refs in unrelated comments: the ticket-archaeology hook scans the whole file, so touching this spec requires clearing them to commit.
Co-Authored-By: Neo Claude Opus neo-claude-opus@neomjs.com"
@neo-opus-grace - 2026-06-04T02:40:49Z
Scope amendment — Layer 3 (access-policy) found + fixed; Layers 1–2 remain open
The operator's redirect ("explore if access related → team vs private; repo default should be team") surfaced a distinct, additional root cause of the empty-summary symptom, beyond the two layers in the body:
Layer 3 — team memorySharing excluded the untagged commons. Session summaries carry no userId metadata, but the team read applied where:{userId:'shared'} → matched nothing → empty. With team as the intended repo default, every summary query read empty. Fixed in PR #12470: team is now additive like legacy (own + 'shared' + untagged commons); private stays strict; codified by a rewritten tenant-isolation test including the cross-tenant-exclusion (other-tenant private stays hidden) assertion.
Layers 1–2 remain open — #12470 does NOT close this ticket. The corruption evidence is raw-Chroma-level (get(embeddings) throws Error finding id; 68/1018 vectors reachable) and independent of access-policy — the Layer-3 fix does not repair the HNSW segment, un-swallow injectQueryReRanker, or add the healthcheck query-canary. Those three ACs stand.
Net:
- Layer 3 (access-policy) AC → delivered (PR #12470, "Part of #12450").
- Layers 1–2 (HNSW corruption repair +
injectQueryReRankerobservability + healthcheck canary) → open.
Recommend #12450 stays open for the corruption + observability work after #12470 merges. (Cross-ref for the reviewer: this reconciles the close-target — #12470 is a partial fix of a multi-cause symptom ticket, not a close of undelivered corruption ACs.)
- 2026-06-04T02:43:35Z @neo-opus-grace cross-referenced by #12473
Context
The
query_summariesMemory Core MCP tool returns{count:0, results:[]}for every semantic query — including plainly-matching ones ("DreamService SemanticGraphExtractor chunked Tri-Vector","REM summarization chunking session","Dream pipeline semantic graph extraction") — while its siblingquery_raw_memoriesreturns rich, relevant results for the identical queries.healthcheckreports the summaries collectionneo-agent-sessionsascount:1018(930 team-shared), providertext-embedding-qwen3-embedding-8b @ 127.0.0.1:1234,status:healthy. So the collection is populated but semantic search over it is 100% non-functional, with no surfaced error.A prior session flagged the same empty-summary-reads pattern and it was dismissed as a "transient glitch." This is not transient — it is a persistent, reproducible defect, and the reason it keeps getting mis-classified as transient is the second half of this ticket (a swallowed error).
The Problem
Verify-Before-Assert was applied before filing; the obvious hypothesis (embedding-model / dimension mismatch) was empirically falsified, and the real root cause was isolated to two layers.
Layer 1 —
neo-agent-sessionsvector segment is corrupt (the trigger)The live Memory Core server log (
mc-server-2026-06-03.log) shows the swallowed failure 14× today:A direct, read-only probe against the live ChromaDB (
localhost:8000) characterizes the corruption precisely:neo-agent-sessions(summaries)neo-agent-memory(raw memories)count()get(include:['metadatas'], limit:2000)query(queryEmbeddings, nResults:300)get(include:['embeddings'], limit:1)Error finding idInterpretation: the metadata/record segment holds all 1018 summaries (
count,getmetadatas,list_summariesall work), but the HNSW vector segment is desynced — only ~68 of 1018 vectors are reachable via ANN search, and materializing stored embeddings throwsError finding idon unresolvable IDs. Dimension mismatch is ruled out (stored == query == 4096; theneo-agent-memorycollection uses the same embedder and is fully healthy). Under the live server's concurrent write load, large-nResultssummary queries traverse into the corrupt region and throw;query_raw_memoriesnever throws becauseneo-agent-memoryis intact. This is the asymmetry the operator observed.Error finding idis a recurring ChromaDB-corruption signature across Memory Core's Chroma collections — previously seen onask_knowledge_base(#12133, closeddropped;#11924fallback-worked-around). It has been routed-around each time rather than root-caused.Layer 2 —
injectQueryReRankerswallows the error into a clean empty result (why it is invisible)StorageRouter.injectQueryReRankerwraps both collections'query()and, on any Pass-1 throw, returns a clean empty result with only alogger.warn:} catch (e) { logger.warn(`[StorageRouter] Pass 1 semantic retrieval failed, falling back to empty result: ${e.message}`); return {ids: [[]], distances: [[]], metadatas: [[]], documents: undefined}; }The tool response (
{count:0, results:[]}) is then indistinguishable from a genuine no-match. Consequences:healthcheckreportsstatus:healthy+count:1018because it only callssummaryCollection.count()(HealthService.mjs:869) and never runs a query canary — a collection thatcount()s but cannotquery()passes the health check.This swallow is the literal Bug-1 prescription from
#9959("wrap Pass 1 ... with graceful fallback to empty result{ids: [[]]}"). That resilience patch — shipped without any upward observability — is now the mechanism hiding live corruption. This ticket is the friction→gold downstream of that decision, not a duplicate of it.The Architectural Reality
ai/services/memory-core/SummaryService.mjs:298querySummaries()— callscollection.query({queryTexts:[query], include:['metadatas','documents']}). Code is correct; it faithfully queries a corrupt collection. (Its siblingMemoryService.queryMemories()atMemoryService.mjs:464is structurally identical — same embedder, same re-ranker — and works only becauseneo-agent-memoryis intact.)ai/services/memory-core/managers/StorageRouter.mjs:65-82injectQueryReRanker()— the Pass-1 try/catch that converts a ChromaDB throw into a silent empty result. Injected on bothmemoryandsummarycollections.ai/services/memory-core/managers/ChromaManager.mjs:221-234getSummaryCollection()— both collections share onecreateDynamicTextEmbeddingFunction({providerResolver: () => aiConfig.embeddingProvider}), confirming embedding is identical across them.ai/services/memory-core/HealthService.mjs:864-873— reportsname/countfor the session collection; no query-path probe.The Fix
Two-layer, single coherent prescription:
neo-agent-sessionsHNSW/vector segment so all 1018 records are query-reachable — re-embed the summaries (the DreamService summary corpus is regenerable) or rebuild the collection index, or restore from a known-good backup. Verify post-repair thatquery_summariesreturns non-empty for plainly-matching queries.injectQueryReRanker— surface the error (or a structureddegradedmarker) up to the tool response so "0 genuine matches" is distinguishable from "query path failed." Add a lightweight per-collection query canary (or a reachable-vector-vs-record-count divergence check) toHealthServiceso a populated collection whosequery()throws or returns near-zero reachable vectors reports a non-healthy/degraded status instead ofstatus:healthy.Contract Ledger Matrix
query_summariesMCP toolai/services/memory-core/SummaryService.mjs:298querySummaries()code:'SUMMARY_QUERY_DEGRADED') — never a silent{count:0,results:[]}ai/mcp/server/memory-core/openapi.yamlError finding idinmc-server-2026-06-03.log; probe asymmetry table aboveStorageRouter.injectQueryReRankerPass-1 catchai/services/memory-core/managers/StorageRouter.mjs:65-82{ids:[[]]}injectQueryReRankerhealthcheckcollection reportai/services/memory-core/HealthService.mjs:864-873degraded+ structured field naming the failing collectionopenapi.yamlhealthcheck schemacount:1018reported healthy while only 68/1018 reachableDecision Record impact
none(no ADR asserted — not verified). The durable fix embodies the general "no silent fallback for a real failure" principle: a resilience fallback that erases the failure signal is worse than the crash it replaced, because it converts a loud, diagnosable error into a silent, multi-day-latent data-availability outage.Acceptance Criteria
neo-agent-sessionsis repaired so all 1018 records are HNSW-reachable;query_summaries({query:'DreamService semantic graph extraction'})returns non-empty. (ops/post-merge verification)injectQueryReRankerno longer returns a clean empty result on a ChromaDB throw — the error or a structureddegradedmarker reaches thequery_summaries/query_raw_memoriestool response. "0 matches" and "query failed" are distinguishable to the caller.healthcheckruns a per-collection query-canary (or reachable-vs-record-count divergence check) and reports a non-healthy/degraded status + structured field when a populated collection's query path throws or has near-zero reachable vectors.query()throws (Error finding idor any ChromaDB error) yields a surfaced error/degraded signal fromquery_summaries, not a silent{count:0}; andhealthcheckreflects degraded for that collection.Error finding idrecurrences.Out of Scope
neo-agent-memoryhealthy on the same embedder).#9959's Bug 2 (active-session summarization leak) — separate concern, tracked there.Avoided Traps
get(embeddings)throws every time), not a flaky concurrent race.Related
#9959— origin of the Pass-1 swallow (its Bug-1 prescription); OPEN. This ticket is its downstream observability consequence.#12133—ask_knowledge_basetype filter throws ChromaDBError finding id(CLOSED,dropped) — sibling corruption signature.#11924— priorError finding idrecurrence onask_knowledge_base(fallback-worked-around).#12180/#12331— Chroma collection hygiene / orphan-collection cleanup (adjacent substrate).Handoff Retrieval Hints
"query_summaries silently empty Error finding id neo-agent-sessions corrupt re-ranker swallow"[StorageRouter] Pass 1 semantic retrieval failed, falling back to empty result: Error executing plan: Internal error: Error finding idquery(queryEmbeddings, nResults:300)caps at 68/1018 onneo-agent-sessions;get(include:['embeddings'])throws.Authored by Claude Opus 4.8 as @neo-opus-ada.