The bug
The vector index could accept a write and then make it unreachable by every possible query — including a search for the record''s own exact text. The victim changed on every index build because graph construction seeds from entropy, so the same database could serve a different subset each time it was opened.
Found when a mounted 65-record knowledge pack served 64 of them. The pack file was complete; the loss happened at mount. A 377-record pack had 2 orphans in the wild.
Any deployment on an earlier version can be holding memories it cannot return.
The fix
Two layers, because the first alone was provably insufficient:
- Layer-0 in-degree tracking — pruning never drops an edge whose target it would orphan. Distance-only truncation was removing the last incoming edge of nodes in dense clusters.
ensure_all_reachable()— restores layer-0 symmetry, then BFS-links any stranded component. Symmetry is the load-bearing part:searchbegins its layer-0 exploration wherever the greedy descent lands, not at the entry point, so entry-point reachability was the wrong guarantee. The first version of this fix passed a targeted test locally and still failed on every CI platform.
Wired into build_vec_index_with_enc, the single funnel shared by host open, rebuild_vec_index, and pack mount.
Upgrading
Nothing to migrate. The index rebuilds from SQLite on open and the repair runs there. Watch for this line in the logs:
vec index rebuild reconnected unreachable nodes
If it fires, that database was holding records it could not return, and the count tells you how many were rescued.
Verification
- New regression test
every_insert_is_reachable_by_its_own_vector: dense cluster plus outliers, five unseeded builds, plus repair idempotence. Reproduced the original failure ~25% of runs; now 12 consecutive passes. - Full workspace suite green on Linux x86_64, Linux aarch64, macOS aarch64 and Windows.
- Orphan census across three packs (489 records, three mounts each): 0 unreachable, against 1–2 per mount before.