Repository navigation
fix(recall): make cold recall fast on large, long-lived stores - #148
Merged
Merged
Conversation
A cold recall paid twice before any ranking started. Ollama unloads a model five minutes after its last request, so an agent that recalls once per turn often reloads a multi-gigabyte embedding model first; the Ollama client now sends keep_alive (MNEMON_EMBED_KEEP_ALIVE, default 30m; not sent to OpenAI-compatible servers). Separately, recall bumps access counters on every hit and nothing ever vacuumed, so a long-lived store ends up with table pages scattered across the file and cold reads run at random-I/O speed. `mnemon gc --compact` runs VACUUM and reports the size before and after. On a large long-lived store on an Apple M4 laptop, VACUUM alone cut a cold-cache recall from about 3.6s to 1.4s. Validated with make test plus new tests for keep_alive (default, override, omitted for OpenAI) and compaction.
Beam search fetched each neighbour with GetInsightByID although recall had already loaded every active insight, re-read a node's edges for every anchor whose neighbourhood reached it, and read edges through single-column indexes that sent each row to a random page of the edges table. Recall also scanned the insights table a second time for the known-entity set. Neighbours now resolve from the loaded snapshot, edges are cached per recall, and the entity set comes from the same snapshot (IntentAwareRecallFrom, store.KnownEntities). New covering indexes on (source_id|target_id, other end, edge_type, weight) answer GetNeighborEdges without touching the table and replace the single-column edge indexes. Its fixed order also makes the visit-budgeted beam deterministic. The superseded check reads a small partial index of supersedes edges once instead of probing the target index per candidate, falling back to the scoped query on stores a read-only open has not migrated. Both open modes set mmap_size. On a large long-lived store (Apple M4, model already loaded, medians of 5-7 runs over three queries) recall went from 0.82-0.87s to 0.53-0.56s warm and from 1.92-1.96s to 1.22-1.53s after purging the page cache, with the same top-10 set. Validated with make test plus tests for neighbour order, query plans, the known-entity set and the missing-index fallback.
Order cached neighborhoods by the full intent and similarity contribution before consuming the visit budget. Relevant causal, temporal, and vector matches must not disappear behind higher raw-weight edges; preserve the existing traversal bounds. Validated focused search/store tests and real CLI comparisons: the causal result returns at rank two in all seven runs, while dense-graph recall retains the performance improvement.
Seek requested target IDs through the covering partial index in bounded batches instead of scanning unrelated supersession history. Retain the read-only legacy-index fallback and exclude self-supersession. Validated query-plan, chunking, and fallback checks plus real CLI comparisons with 100,000 historical edges: median query time returns from 29.09 ms to 12.69 ms with unchanged results.
Inspect the checkpoint result rows before and after VACUUM, report blocked or partially completed compaction accurately, and propagate database size errors instead of returning a false success. Validated real concurrent-reader CLI reproduction and SQLite tests for busy rejection, successful retry with committed data preserved, and read-only rejection. Normal compaction preserves content and passes integrity_check.
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Durable-store maintenance and budget-sensitive traversal need human validation, and a transaction deadlock remains unresolved.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Improves cold Memory recall by reducing database reads and embedding-model reloads, with explicit store compaction.
Changes:
- Adds configurable Ollama model retention and
gc --compact. - Reuses insight snapshots and cached transitions; adds recall indexes and memory-mapped reads.
- Adds regression tests and English/Chinese documentation.
| File | Description |
|---|---|
| README.md | Documents model retention setting. |
| internal/memory/store/recall_index_test.go | Tests indexes, ordering, entity parity, and fallback. |
| internal/memory/store/node.go | Extracts known entities from loaded insights. |
| internal/memory/store/edge.go | Adds optimized neighbor and supersession queries. |
| internal/memory/store/db.go | Adds recall indexes and memory-mapped reads. |
| internal/memory/store/compact.go | Implements checkpointed compaction. |
| internal/memory/store/compact_test.go | Tests space reclamation. |
| internal/memory/store/compact_busy_test.go | Tests busy and read-only rejection. |
| internal/memory/search/recall.go | Reuses snapshots and caches transitions. |
| internal/memory/search/recall_transition.go | Ranks transitions before visit limits. |
| internal/memory/search/recall_budget_test.go | Tests budget-sensitive ranking. |
| internal/memory/search/integration_test.go | Adapts traversal tests. |
| internal/memory/embed/ollama.go | Sends configurable Ollama retention. |
| internal/memory/embed/ollama_test.go | Tests retention defaults and protocol handling. |
| docs/zh/USAGE.md | Documents new options in Chinese. |
| docs/zh/README.md | Documents model retention in Chinese. |
| docs/USAGE.md | Documents compaction and model retention. |
| cmd/memory/recall.go | Shares the active-insight snapshot. |
| cmd/memory/gc.go | Exposes compaction and size reporting. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if db.readOnly { | ||
| return 0, 0, fmt.Errorf("compact: database is read-only") | ||
| } | ||
| if err := db.compactCheckpoint(); err != nil { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What
Two changes that make cold
mnemon recallfast on large, long-lived stores:feat(memory): the Ollama client sendskeep_alive(newMNEMON_EMBED_KEEP_ALIVE, default30m; not sent to OpenAI-compatible servers). A newmnemon gc --compactrunsVACUUMand reports the file size before and after.fix(recall): cuts the per-node SQLite reads in beam search:GetInsightByIDeach;IntentAwareRecallFrom,store.KnownEntities), so the insights table is scanned once;GetNeighborEdgesfrom the index alone, in a fixed order;supersedesedges serves the superseded check;mmap_size.Why
On a large, long-lived store, a cold recall took about 5.8s before this change. The time was mostly waste:
keep_aliveunloads it between agent turnsVACUUMalone cut a cold-cache recall from ~3.6s to ~1.4s.Profiling the remaining time found several other costs in beam search:
GetInsightByIDcalls and ~1k uncachedGetEdgesByNodecalls, around 440ms even when warm.Measurements after the recall change (Apple M4, embedding model already loaded, medians of 5–7 runs over three queries):
Each query returned the same top-10 set.
Notes for review
migrateEdgeRecallIndexesruns last on every writable open, because the edge-table rebuild migrations recreate the single-column indexes. It addsidx_edges_source_cover,idx_edges_target_coverand the partialidx_edges_supersedes, and dropsidx_edges_source/idx_edges_target. The(id, edge_type)indexes still serve plain id lookups. The first open builds the indexes (under a second with hundreds of thousands of edges). The file grows by ~15%.IN (...)query whenidx_edges_supersedesis absent (covered by a test).INDEXED BYis needed because SQLite's planner will not choose a partial index for this query by itself.GetNeighborEdgesorders byweight DESCplus a key tiebreak. Beam search stops at a visit budget, so which neighbours it reaches depended on index order. This makes it deterministic, and it can reorder lower-ranked results relative to before.source_id != target_id); the partial index covers(target_id, source_id)so the filter stays index-only.gc --compactis explicit rather than automatic, because free-page counts alone do not reveal page scattering. It needs an exclusive lock and briefly doubles disk use.Checklist
make test)make test-integration, when affected)keep_alivedefault, override and OpenAI omission; compaction; neighbour order; query plans use the covering and partial indexes; known-entity parity; missing-index fallbackMNEMON_EMBED_KEEP_ALIVEandgc --compactMNEMON_EMBED_KEEP_ALIVE(default keeps the model loaded 30 minutes); newmnemon gc --compact; faster recall; a one-time index migration on the first writable open