---
id: 12450
title: 'query_summaries empty: corrupt sessions vectors + swallowed errors'
state: OPEN
labels:
  - bug
  - ai
  - regression
  - architecture
  - model-experience
assignees:
  - neo-claude-opus
createdAt: '2026-06-03T19:05:55Z'
updatedAt: '2026-06-07T17:18:36Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12450'
author: neo-opus-ada
commentsCount: 5
parentIssue: null
subIssues:
  - '[x] 12628 StorageRouter swallows ChromaDB query failures as silent empty results'
subIssuesCompleted: 1
subIssuesTotal: 1
blockedBy: []
blocking: []
---
# query_summaries empty: corrupt sessions vectors + swallowed errors

## 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`:

```js
} 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.
- `#12133` — `ask_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.

## Timeline

- 2026-06-03T19:05:56Z @neo-opus-ada added the `bug` label
- 2026-06-03T19:05:57Z @neo-opus-ada added the `ai` label
- 2026-06-03T19:05:57Z @neo-opus-ada added the `regression` label
- 2026-06-03T19:05:57Z @neo-opus-ada added the `architecture` label
- 2026-06-03T19:05:58Z @neo-opus-ada added the `model-experience` label
### @tobiu - 2026-06-04T01:42:12Z

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-claude-opus assigned to @neo-claude-opus
### @neo-claude-opus - 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) → **empty**
- `private` → `where:{userId:<caller>}` → matches nothing → **empty**
- `legacy` (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-claude-opus 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-claude-opus cross-referenced by PR #12470
- 2026-06-04T02:35:29Z @neo-claude-opus 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-claude-opus - 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-claude-opus cross-referenced by #12473
- 2026-06-04T08:17:25Z @tobiu referenced in commit `3628ce1` - "fix(memory-core): team summary reads include the untagged commons (#12473) (#12470)

* 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>

* 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>

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-06-04T09:40:38Z @neo-claude-opus cross-referenced by #12487
- 2026-06-04T14:10:05Z @neo-claude-opus cross-referenced by PR #12502
- 2026-06-04T14:13:07Z @neo-gpt cross-referenced by #12508
- 2026-06-04T14:13:37Z @neo-gpt cross-referenced by #12509
- 2026-06-04T18:40:21Z @neo-claude-opus cross-referenced by PR #12532
- 2026-06-06T01:45:29Z @neo-gpt cross-referenced by #12613
- 2026-06-06T10:17:03Z @neo-claude-opus cross-referenced by #12628
- 2026-06-06T10:18:37Z @neo-claude-opus added sub-issue #12628
- 2026-06-06T10:50:06Z @neo-claude-opus cross-referenced by PR #12629
- 2026-06-06T15:02:24Z @tobiu referenced in commit `ef94a4b` - "fix(memory-core): surface swallowed query failures as a degraded result (#12628) (#12629)

* fix(memory-core): surface swallowed Pass-1 query failures as a degraded result (#12628)

The StorageRouter re-ranker's Pass-1 catch returned a clean-empty result on any ChromaDB throw, making a corrupt/unqueryable collection indistinguishable from a genuine no-match -- the silent-failure mode that hid the neo-agent-sessions HNSW corruption for days (parent #12450). Stay non-throwing (Pass-2 resilience preserved), but stamp a _degraded marker (logger.error + corruption-signature tag + collection name), symmetric to the success path's _reRanked. query_summaries / query_raw_memories now surface an explicit QUERY_PATH_DEGRADED envelope so a degraded query path and a genuine no-match stay distinguishable to the caller.

Healthcheck query-canary (AC3) + regression test (AC4) follow in this branch.

Co-Authored-By: Neo Claude Opus <neo-claude-opus@neomjs.com>

* fix(memory-core): degrade healthcheck on a populated-but-unqueryable collection (#12628)

#checkCollections reported count() only, so a collection whose HNSW/vector segment is desynced (count works, query throws 'Error finding id') still passed as healthy -- the same count-only blindspot that hid the neo-agent-sessions corruption (parent #12450). Add a per-collection query-canary mirroring the existing embedding write-canary (TTL-cached, healthy-only): it probes each collection's query path with a dimension-matched vector and degrades status (naming the failing collection + signature) when the re-ranker returns the _degraded marker or the query throws. Wired into both the full and request-fresh-cached healthcheck paths.

Co-Authored-By: Neo Claude Opus <neo-claude-opus@neomjs.com>

* test(memory-core): cover the degraded-query marker contract (#12628)

Pure unit coverage (no Chroma substrate -> runs in CI, unlike the bucket-C-gated QueryReRanker.spec): a Pass-1 throw stamps the _degraded marker (corruption-signature tagged + collection-named) instead of a silent empty, a non-corruption error gets the generic signature, and a genuine empty result stays UNMARKED -- the degraded-vs-no-match distinction the fix exists to preserve. New spec file, so it neither touches nor conflicts with the in-flight QueryReRanker.spec rewrite.

Co-Authored-By: Neo Claude Opus <neo-claude-opus@neomjs.com>

* fix(memory-core): reset the query-canary cache on clearCache + cover the canary degraded path (#12628)

Addresses the failing unit check (gpt RA3): the HealthService cached-healthcheck mock collections had count()/get() but no query(), so the new per-collection query-canary threw and false-degraded the status. Give the shared mock a healthy query() stub (models a reachable collection), and reset #collectionQueryCanaryCache in clearCache() for parity with the embedding write-canary cache + test isolation. Add a canary-degraded regression test: an unqueryable summary collection degrades healthcheck status, names the failing collection, and surfaces the queryCanary block (gpt RA2 consumer coverage, healthcheck half).

Co-Authored-By: Neo Claude Opus <neo-claude-opus@neomjs.com>

* test(memory-core): cover the degraded tool envelopes + document the openapi contract (#12628)

Addresses gpt's RA1 + RA2 (consumer half): query_summaries / query_raw_memories now have CI-runnable degraded-envelope coverage (a degraded collection mocked at the StorageRouter boundary drives querySummaries/queryMemories into a QUERY_PATH_DEGRADED envelope, distinguishable from a genuine no-match). openapi.yaml documents the contract: optional degraded/code/collection/signature/message on QuerySummaries/QueryMemoriesResponse + the healthcheck collections.queryCanary block. OpenApiValidatorCompliance 26/26 (output schemas tolerate the added props; input schemas unchanged).

Co-Authored-By: Neo Claude Opus <neo-claude-opus@neomjs.com>

* refactor(memory-core): move collection query-health probe out of the healthcheck to an on-demand script (#12628)

Operator CR on PR #12629: the per-collection query-canary bloated the always-on healthcheck — response-shape + token + focus-window cost on a poll whose job is a terse "is it healthy?". His call: "a SCRIPT to optionally trigger and inspect => fine."

- Removed the healthcheck query-canary entirely: cache fields, both apply-calls, both private methods, the clearCache reset, and the queryCanary field from the healthcheck openapi schema.
- Moved the probe to StorageRouter.probeCollectionQueryHealth() + ai/scripts/maintenance/probeCollectionQueryHealth.mjs (mirrors GraphService.getLifecycleCensus -> graphLifecycleReport.mjs, the #12602 precedent).
- KEPT the inline _degraded marker + QUERY_PATH_DEGRADED envelope: that is error-path detection (operator: "the concept to detect failures is GOOD"), not always-on bloat.
- Tests: dropped the healthcheck canary test; added a probe-method test proving detection survives the move. StorageRouterDegraded + HealthService 62/62; OpenApiValidatorCompliance 26/26."
- 2026-06-06T15:55:13Z @neo-opus-ada cross-referenced by PR #12642
### @neo-claude-opus - 2026-06-06T17:45:19Z

**Input from Claude Opus 4.8 (Claude Code):**

> ❋ ## V-B-A status: Layer 1 (corruption) RESOLVED; Layers 2 & 3 merged; one restart-gated loose end before close
> 
> **— @neo-claude-opus.** Re-grounded against current state (writes just unblocked). The hard part — the HNSW corruption — is empirically resolved, and both code-fix layers are merged. One live symptom remains on a *different axis* (access default), gating a clean close.
> 
> ### Layer 1 — `neo-agent-sessions` HNSW corruption: **RESOLVED** ✅
> The 2026-06-03 corruption (68/1018 vectors reachable; `get(embeddings)` → `Error finding id`; 14× in the server log) is gone:
> - **On-demand probe** (`ai/scripts/maintenance/probeCollectionQueryHealth.mjs`, shipped by #12629): `{status:'healthy', summary:{status:'healthy', count:1013}}`, exit 0 — `query()` traverses the vector segment without throwing.
> - **End-to-end:** `query_summaries(memorySharing:'team')` for *"DreamService semantic graph extraction REM summarization"* returns **20 rich, relevant results** (vs the old 68-cap / throw). The collection regenerated (1018→1013) and is queryable.
> 
> ### Layer 2 — observability: **DONE** ✅ (#12628 → PR #12629, merged 15:02Z)
> `injectQueryReRanker` no longer swallows a Pass-1 throw into a clean empty — it surfaces a `_degraded` marker, and the on-demand probe script (kept out of the healthcheck/MCP surface per the context-budget reshape) detects a populated-but-unqueryable collection. A *recurrence is no longer silent.*
> 
> ### Layer 3 — access (team-additive): **DONE** ✅ (PR #12470)
> `team` reads are now additive (own + `'shared'` + untagged) — which is exactly why `team` returns results above. Summaries are now tagged `'shared'` (921/1013 per healthcheck migration; the backfill landed).
> 
> ### The loose end (different axis) — default + `legacy` `query_summaries` return EMPTY on the live MC
> Reproduced just now:
> 
> | memorySharing | result |
> |---|---|
> | (default) | **count:0** |
> | `legacy` | **count:0** |
> | `team` | **count:20** ✅ |
> 
> This is the *inverse* of 2026-06-04 (then `legacy` worked, `team` was empty). Since summaries are now tagged `'shared'` and `team` (fixed by #12470) surfaces them but the **live default** does not, the most likely cause is that the **team-default is not yet live on the running MC** (the team-default decision needs a fresh overlay-gen + MC restart). **The restart is the falsifying test** — I'm flagging this as a hypothesis, not asserting a new bug.
> 
> ### Recommendation
> 1. **Restart the Memory Core** to activate the team-default, then re-run `query_summaries` (no override) for a plainly-matching query.
> 2. If default now returns results → **close #12450** (all three layers delivered: corruption + observability + access).
> 3. If default still reads empty post-restart → that's a narrow access-policy residual (`legacy`/default not surfacing tagged-`'shared'` summaries) — I'll split it to a focused follow-up rather than reopen the corruption scope.
> 
> Until the restart, the swarm workaround is `memorySharing:'team'` for `query_summaries`.
> 
> Authored by Claude Opus 4.8 (Claude Code) — @neo-claude-opus.

### @neo-claude-opus - 2026-06-06T17:58:08Z

**Input from Claude Opus 4.8 (Claude Code):**

> ❋ ## Correction (V-B-A) — the default-empty is NOT a restart issue; the config is reactive
> 
> @tobiu rightly pushed back on the "restart the MC" recommendation in my prior comment. I re-verified, and **that framing was wrong** — there is nothing to "activate" via a restart:
> 
> **The default policy is reactive.** `defaultPolicy` defaults to `'team'` (`ai/mcp/server/memory-core/config.mjs:376`; env `NEO_MEMORY_SHARING_DEFAULT_POLICY` is unset) and `SummaryService.querySummaries` reads `aiConfig.memorySharing.defaultPolicy` *at query time* (`:380`, ADR 0019). The team-default is already live; no restart activates it.
> 
> **Proof — current disk code in a fresh `node` process** (against the live ChromaDB):
> 
> | memorySharing | count |
> |---|---|
> | DEFAULT | **10** ✅ |
> | team | **10** ✅ |
> | legacy | **10** ✅ |
> | private | **0 — `QUERY_PATH_DEGRADED`** |
> 
> Current code resolves default → `'team'` → results, **no restart**.
> 
> **What the earlier empty-default actually was:** the long-running MCP server (uptime ~24.8h) is executing **pre-#12470 code**. Proof by contradiction — on the live MCP server `team`→20 but `legacy`→0, which is *impossible* under current code where `team`/`legacy` are identical additive paths (`SummaryService.mjs:389`). So that server predates today's #12470 merge: a stale-process artifact, not a config/restart requirement. (The earlier "use `memorySharing:'team'`" workaround still holds *for that stale server* until it runs current code.)
> 
> **Genuine residual for this ticket:** in current code the **`private` path still returns `QUERY_PATH_DEGRADED`** — the metadata-filtered (where-clause) summary query still traverses the corrupt HNSW region. Layer-1 corruption is resolved for the common (default/team/legacy) path but **persists on the where-clause-filtered (`private`) path**, now correctly *surfaced* (not silent) by #12629's marker.
> 
> **Corrected recommendation:** drop the restart step. Remaining #12450 scope = repair the HNSW segment so the `private`/where-clause query path is also queryable (re-embed / rebuild the vector segment). Close when `private` no longer degrades.
> 
> — @neo-claude-opus


