perf(agent): persist the vector index graph; incremental catch-up

open() marked the HNSW index dirty, so the first search of every session
rebuilt it from scratch — 36 s at 100K records with the (better, slower)
heuristic build. First query after open is now 1.7 / 15 / 159 ms at
1K / 10K / 100K; what remains is the one-off keyword index build.

- clawhdf5-ann: HnswIndex::graph_to_bytes / from_graph_bytes serialize the
  graph only (levels, tombstones, adjacency as u32, CRC32). The existing HDF5
  serializer embeds a full copy of every vector, which would double a store
  that already holds them. Loading validates everything — counts, levels vs
  layer count, connection limits, every neighbour id and the layer it must
  exist on — so a damaged graph, or a hostile one with a valid checksum, is an
  error rather than an out-of-bounds walk during search.
- clawhdf5-agent: each checkpoint writes the graph to <store>.h5.ann (synced,
  atomic, before the .h5) and records a fresh generation id in /meta. open()
  loads the sidecar only if its generation matches that checkpoint; missing,
  stale, damaged or mismatched sidecars are ignored and the index rebuilt.
  Records appended through WAL replay join the loaded index incrementally; a
  replayed Update or Tombstone invalidates it. snapshot() copies it. Only an
  index that exactly mirrors the cache is saved; otherwise a stale sidecar is
  removed.
- ensure_hnsw_fresh inserts records appended since the last sync instead of
  rebuilding, so save_batch no longer marks the whole index dirty.
- CheckpointMeta { wal_applied, ann_generation } with *_with_meta build/write/
  read functions; the *_with_mark ones delegate.
- Harness reports the one-off cold index build separately from the first query
  after a reopen.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 08:00:57 -07:00
co-authored by Claude Fable 5.1
parent 2bfbb7fb4b
commit 0ee698accd
7 changed files with 641 additions and 20 deletions
+11
View File
@@ -23,6 +23,17 @@
immediately; a crash in between forgets only the boosts since the last
checkpoint. Activation weights are now capped (16.0) - they grew without
bound.
- `clawhdf5-agent`: **the vector index is persisted**, so `open()` no longer
rebuilds it on the first search (first query after open: 2627 -> 15 ms at 10K
records, 36 s -> 159 ms at 100K). The HNSW graph — not the vectors, which the
store already holds — is written to `<store>.h5.ann` at each checkpoint and
tied to it by a generation id in `/meta`; a missing, stale, damaged or
structurally invalid sidecar is ignored and the index rebuilt. Records
replayed from the WAL join the loaded index incrementally; a replayed update
or delete invalidates it. `snapshot()` copies it. Batch saves no longer force
a full index rebuild.
- `clawhdf5-ann`: `HnswIndex::graph_to_bytes` / `from_graph_bytes` — graph-only
serialization (checksummed, every neighbour id and level validated on load).
- `clawhdf5-agent`: BM25 results are deterministic (ties break by record id),
top-k uses a bounded heap, and the "WAND early termination" that computed a
bound and then ignored it is gone. IDF is computed per query.