Problem
`Layer3.search_raw()` in `mempalace/layers.py` has the same unguarded `meta.get(...)` pattern that #1007 / PR #999 fix in `searcher.py`. When ChromaDB's `query()` returns `None` for any metadata entry (partial-flush, mid-delete, schema upgrade boundaries), this path crashes with:
```
AttributeError: 'NoneType' object has no attribute 'get'
```
Where
`mempalace/layers.py`, around lines 300-341 — the `Layer3.search_raw` method iterates directly over `col.query(**kwargs)` results and calls `meta.get("wing", "unknown")`, `meta.get("room", "unknown")`, and `meta.get("source_file", "?")` without any null safety.
Root cause
Same as #1007: ChromaDB `query()` can return `None` in the `metadatas` list when a drawer's HNSW vector entry exists but its metadata row hasn't been materialized yet. #1006 documented this happens routinely on chromadb 1.5.x; even after the chromadb pin lands, partial-state results remain possible during interrupted mines or schema upgrades.
Proposed fix
Same two-line defensive coercion as #999 applies in `searcher.py`:
```diff
for i, (doc, meta, dist) in enumerate(zip(docs, metas, dists), 1):
- meta = meta or {}
- doc = doc or ""
... meta.get(...) calls that follow
```
Apply at the top of the results loop in `Layer3.search_raw()` so every downstream `meta.get()` and `doc.strip()` (or equivalent) is safe.
Relationship to other PRs
Good first issue
Small scope, established pattern (can copy from #999 / PR #1009 approach), self-contained in one file.
Why separate from #1007
Kept separate because `layers.py` is a different file with different test fixtures. PR #999 scoped itself to `searcher.py` + `miner.status()`; this is the third sibling that should land as a standalone PR to keep diffs reviewable.
Problem
`Layer3.search_raw()` in `mempalace/layers.py` has the same unguarded `meta.get(...)` pattern that #1007 / PR #999 fix in `searcher.py`. When ChromaDB's `query()` returns `None` for any metadata entry (partial-flush, mid-delete, schema upgrade boundaries), this path crashes with:
```
AttributeError: 'NoneType' object has no attribute 'get'
```
Where
`mempalace/layers.py`, around lines 300-341 — the `Layer3.search_raw` method iterates directly over `col.query(**kwargs)` results and calls `meta.get("wing", "unknown")`, `meta.get("room", "unknown")`, and `meta.get("source_file", "?")` without any null safety.
Root cause
Same as #1007: ChromaDB `query()` can return `None` in the `metadatas` list when a drawer's HNSW vector entry exists but its metadata row hasn't been materialized yet. #1006 documented this happens routinely on chromadb 1.5.x; even after the chromadb pin lands, partial-state results remain possible during interrupted mines or schema upgrades.
Proposed fix
Same two-line defensive coercion as #999 applies in `searcher.py`:
```diff
for i, (doc, meta, dist) in enumerate(zip(docs, metas, dists), 1):
... meta.get(...) calls that follow
```
Apply at the top of the results loop in `Layer3.search_raw()` so every downstream `meta.get()` and `doc.strip()` (or equivalent) is safe.
Relationship to other PRs
mempalace searchcrashes withAttributeError: 'NoneType' object has no attribute 'get'on partial-state results (searcher.py:286) #1007 + PR fix(searcher): guard against None metadata in CLI print path #999 — same class of bug, covers `searcher.py` `search()` + `search_memories()` + `miner.status()`. Does NOT cover `Layer3.search_raw()` (different file).pip install mempalacesilently broken on chromadb 1.5.x — writes queued but never flush, search returns ghosts #1006 — root cause (chromadb 1.5.x queue-stall makes partial-flush states the common case today)Good first issue
Small scope, established pattern (can copy from #999 / PR #1009 approach), self-contained in one file.
Why separate from #1007
Kept separate because `layers.py` is a different file with different test fixtures. PR #999 scoped itself to `searcher.py` + `miner.status()`; this is the third sibling that should land as a standalone PR to keep diffs reviewable.