LearnNewsExamplesServices
Frontmatter
id12450
titlequery_summaries empty: corrupt sessions vectors + swallowed errors
stateClosed
labels
bugairegressionarchitecturemodel-experience
assigneesneo-opus-grace
createdAtJun 3, 2026, 9:05 PM
updatedAtJun 21, 2026, 3:48 PM
githubUrlhttps://github.com/neomjs/neo/issues/12450
authorneo-opus-ada
commentsCount13
parentIssuenull
subIssues
12628 StorageRouter swallows ChromaDB query failures as silent empty results
subIssuesCompleted1
subIssuesTotal1
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 18, 2026, 4:19 AM

query_summaries empty: corrupt sessions vectors + swallowed errors

Closed v13.1.0/archive-v13-1-0-chunk-1 bugairegressionarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on Jun 3, 2026, 9:05 PM

Context

The query_summaries Memory 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 sibling query_raw_memories returns rich, relevant results for the identical queries. healthcheck reports the summaries collection neo-agent-sessions as count:1018 (930 team-shared), provider text-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-sessions vector segment is corrupt (the trigger)

The live Memory Core server log (mc-server-2026-06-03.log) shows the swallowed failure 14× today:

[WARN] [StorageRouter] Pass 1 semantic retrieval failed, falling back to empty result: Error executing plan: Internal error: Error finding id

A direct, read-only probe against the live ChromaDB (localhost:8000) characterizes the corruption precisely:

Probe neo-agent-sessions (summaries) neo-agent-memory (raw memories)
count() 1018 15870
get(include:['metadatas'], limit:2000) OK (1018 ids) OK
query(queryEmbeddings, nResults:300) caps at 68 hits returns 300
get(include:['embeddings'], limit:1) THROWS Error finding id healthy
query embedding dim vs stored dim 4096 == 4096 4096 == 4096

Interpretation: the metadata/record segment holds all 1018 summaries (count, get metadatas, list_summaries all work), but the HNSW vector segment is desynced — only ~68 of 1018 vectors are reachable via ANN search, and materializing stored embeddings throws Error finding id on unresolvable IDs. Dimension mismatch is ruled out (stored == query == 4096; the neo-agent-memory collection uses the same embedder and is fully healthy). Under the live server's concurrent write load, large-nResults summary queries traverse into the corrupt region and throw; query_raw_memories never throws because neo-agent-memory is intact. This is the asymmetry the operator observed.

Error finding id is a recurring ChromaDB-corruption signature across Memory Core's Chroma collections — previously seen on ask_knowledge_base (#12133, closed dropped; #11924 fallback-worked-around). It has been routed-around each time rather than root-caused.

Layer 2 — injectQueryReRanker swallows the error into a clean empty result (why it is invisible)

StorageRouter.injectQueryReRanker wraps both collections' query() and, on any Pass-1 throw, returns a clean empty result with only a logger.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:

  • A corrupt collection reads as "no results found" to every agent — masking the real failure for days and earning the "transient glitch" mis-diagnosis.
  • healthcheck reports status:healthy + count:1018 because it only calls summaryCollection.count() (HealthService.mjs:869) and never runs a query canary — a collection that count()s but cannot query() 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:298 querySummaries() — calls collection.query({queryTexts:[query], include:['metadatas','documents']}). Code is correct; it faithfully queries a corrupt collection. (Its sibling MemoryService.queryMemories() at MemoryService.mjs:464 is structurally identical — same embedder, same re-ranker — and works only because neo-agent-memory is intact.)
  • ai/services/memory-core/managers/StorageRouter.mjs:65-82 injectQueryReRanker() — the Pass-1 try/catch that converts a ChromaDB throw into a silent empty result. Injected on both memory and summary collections.
  • ai/services/memory-core/managers/ChromaManager.mjs:221-234 getSummaryCollection() — both collections share one createDynamicTextEmbeddingFunction({providerResolver: () => aiConfig.embeddingProvider}), confirming embedding is identical across them.
  • ai/services/memory-core/HealthService.mjs:864-873 — reports name/count for the session collection; no query-path probe.

The Fix

Two-layer, single coherent prescription:

  1. Repair the data (immediate remediation). Rebuild the neo-agent-sessions HNSW/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 that query_summaries returns non-empty for plainly-matching queries.
  2. Close the observability hole (the durable, testable fix). Stop returning a clean empty result on a ChromaDB throw in injectQueryReRanker — surface the error (or a structured degraded marker) 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) to HealthService so a populated collection whose query() throws or returns near-zero reachable vectors reports a non-healthy/degraded status instead of status:healthy.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
query_summaries MCP tool ai/services/memory-core/SummaryService.mjs:298 querySummaries() On query-path error, return an explicit degraded/error envelope (e.g. code:'SUMMARY_QUERY_DEGRADED') — never a silent {count:0,results:[]} Controlled error surface; no silent empty ai/mcp/server/memory-core/openapi.yaml 14× Error finding id in mc-server-2026-06-03.log; probe asymmetry table above
StorageRouter.injectQueryReRanker Pass-1 catch ai/services/memory-core/managers/StorageRouter.mjs:65-82 Propagate/annotate the ChromaDB error (degraded marker) upward instead of returning clean empty {ids:[[]]} Re-raise with context, or typed degraded result consumed by callers JSDoc on injectQueryReRanker The swallow block + the 14 swallowed warns
healthcheck collection report ai/services/memory-core/HealthService.mjs:864-873 Add per-collection query-canary / reachable-vs-record divergence; degrade status when a populated collection cannot query Status degraded + structured field naming the failing collection openapi.yaml healthcheck schema count:1018 reported healthy while only 68/1018 reachable

Decision 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-sessions is repaired so all 1018 records are HNSW-reachable; query_summaries({query:'DreamService semantic graph extraction'}) returns non-empty. (ops/post-merge verification)
  • injectQueryReRanker no longer returns a clean empty result on a ChromaDB throw — the error or a structured degraded marker reaches the query_summaries / query_raw_memories tool response. "0 matches" and "query failed" are distinguishable to the caller.
  • healthcheck runs 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.
  • Regression test: a collection whose query() throws (Error finding id or any ChromaDB error) yields a surfaced error/degraded signal from query_summaries, not a silent {count:0}; and healthcheck reflects degraded for that collection.
  • Document the corruption's origin (interrupted re-embed / migration / Chroma version event) so it does not silently recur; cross-link the prior Error finding id recurrences.

Out of Scope

  • Changing the embedding provider/model or vector dimensions — V-B-A falsified the dimension-mismatch hypothesis (stored == query == 4096; neo-agent-memory healthy on the same embedder).
  • #9959's Bug 2 (active-session summarization leak) — separate concern, tracked there.
  • A general ChromaDB version upgrade (may be a cause to document, but the fix here is repair + observability, not an upgrade).

Avoided Traps

  • Misdiagnosing as embedding-model/dimension mismatch — the intuitive first hypothesis. Falsified empirically: identical 4096-dim embedder, sibling collection healthy.
  • "Just clear and re-add the summaries" and close it — fixes today's symptom but leaves the silent-failure mode that hid the corruption for days; it would recur invisibly. The observability fix is the load-bearing half.
  • Treating it as a transient infra hiccup — the corruption is deterministic in an isolated probe (68/1018 reachable; 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.
  • #12133ask_knowledge_base type filter throws ChromaDB Error finding id (CLOSED, dropped) — sibling corruption signature.
  • #11924 — prior Error finding id recurrence on ask_knowledge_base (fallback-worked-around).
  • #12180 / #12331 — Chroma collection hygiene / orphan-collection cleanup (adjacent substrate).

Handoff Retrieval Hints

  • Retrieval Hint: "query_summaries silently empty Error finding id neo-agent-sessions corrupt re-ranker swallow"
  • Log signature: [StorageRouter] Pass 1 semantic retrieval failed, falling back to empty result: Error executing plan: Internal error: Error finding id
  • Probe shape: direct ChromaDB query(queryEmbeddings, nResults:300) caps at 68/1018 on neo-agent-sessions; get(include:['embeddings']) throws.

Authored by Claude Opus 4.8 as @neo-opus-ada.

tobiu
tobiu Jun 4, 2026, 3:42 AM

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:

  • teamwhere:{userId:'shared'} → matches nothing (records untagged) → empty
  • privatewhere:{userId:<caller>} → matches nothing → empty
  • legacy (default) → no DB-where + JS post-filter !userId || ===caller || ===sharedpasses 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 + injectQueryReRanker observability + 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
tobiu referenced in commit 3628ce1 - "fix(memory-core): team summary reads include the untagged commons (#12473) (#12470) on Jun 4, 2026, 10:17 AM
tobiu referenced in commit ef94a4b - "fix(memory-core): surface swallowed query failures as a degraded result (#12628) (#12629) on Jun 6, 2026, 5:02 PM
tobiu referenced in commit 60b7f2c - "fix(memory-core): include 'distances' in query — restore re-ranker semantic score (#13222) (#13223) on Jun 14, 2026, 2:35 PM
tobiu referenced in commit 2d331e8 - "docs(incident): agent identity drift forensic record + runbook (#13239) (#13240) on Jun 14, 2026, 6:41 PM
tobiu closed this issue on Jun 18, 2026, 4:19 AM