Skip to content

feat(memory): migrate brain DB onto agentflare-store engine + hybrid recall - #258

Merged
getappz merged 8 commits into
masterfrom
task/188
Jul 19, 2026
Merged

getappz merged 8 commits into
masterfrom
task/188

Conversation

@getappz

@getappz getappz commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

Migrates the memory brain DB (brain.db) onto the shared agentflare-store / agentflare-db-kit engine and adds optional hybrid recall (BM25 + vector) that degrades cleanly to today's FTS-only behavior when no embedding model is present.

What changed

  • agentflare-store::retrieval — pure, dependency-free merge/decay/MMR/jaccard math ported from openpawz's memory engine.
  • brain.db → db-kit engine — open() now goes through db_kit::open_file with a versioned Migrations list (frozen V1_DDL). FK enforcement and unix perms preserved; hand-rolled tune() deleted.
  • observations_vec table + embeddings module — vector storage plus missing/candidates helpers, every query filtered on deleted_at IS NULL.
  • semantic cargo feature — OnceLock ONNX engine that returns None everywhere on non-semantic builds, missing model, or a failed call.
  • search_hybrid wired into recall — merges FTS + vector (0.4/0.6), temporal decay (30d half-life), MMR rerank (lambda=0.7). With no embedder it is byte-identical to search().
  • agentflare memory backfill-embeddings CLI + CHANGELOG entry.

Verification

  • cargo fmt --check passes
  • cargo clippy --workspace --all-targets --all-features -D warnings clean for all authored code
  • cargo test --workspace — 35 test binaries ok, 0 failed

Resolves review blockers A-G (FK enforcement, soft-delete leak, error handling, dual schema, project scoping, duplicate-outcome corruption, missing tests) by construction.

Summary by CodeRabbit

  • New Features

    • Added optional semantic memory recall that blends keyword and vector results with recency decay and diversity-aware re-ranking (enabled via the semantic feature).
    • Added embedding persistence for saved observations.
    • Added memory backfill-embeddings command to populate embeddings for existing memories.
  • Improvements

    • Memory database now uses versioned migrations, write-ahead logging, and foreign-key enforcement.
    • When embeddings are unavailable, keyword-only recall behavior remains unchanged.
  • Bug Fixes

    • Improved upgrades for legacy databases while preserving existing memory data.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d5564690-8dcf-418f-b8d8-87297105fc63

📥 Commits

Reviewing files that changed from the base of the PR and between 1c1ae66 and 7dc2e6e.

📒 Files selected for processing (7)
  • src/memory/embeddings.rs
  • src/memory/mcp.rs
  • src/memory/observations.rs
  • src/memory/relations.rs
  • src/memory/search.rs
  • src/memory/sessions.rs
  • src/memory/summaries.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/memory/observations.rs
  • src/memory/summaries.rs
  • src/memory/mcp.rs
  • src/memory/sessions.rs
  • src/memory/embeddings.rs
  • src/memory/search.rs

📝 Walkthrough

Walkthrough

The memory subsystem now uses versioned SQLite migrations, stores optional observation embeddings, supports feature-gated hybrid semantic recall with FTS fallback, and provides a CLI command for embedding backfill.

Changes

Memory and semantic retrieval

Layer / File(s) Summary
Versioned memory database migrations
Cargo.toml, src/memory/schema.rs, src/memory/store.rs, src/memory/{observations,relations,sessions,summaries}.rs
The database uses db-kit and versioned migrations, adds observations_vec, enables WAL and foreign keys, and verifies legacy database upgrades.
Embedding storage and optional engine
Cargo.toml, crates/agentflare-store/src/embed.rs, src/memory/{mod,embeddings,engine}.rs
Embedding vectors gain little-endian serialization, SQLite persistence and cosine candidate lookup, while ONNX embedding generation is enabled only with the semantic feature.
Retrieval scoring utilities
crates/agentflare-store/src/{lib,retrieval}.rs
Reusable BM25/vector score fusion, temporal decay, MMR selection, and Jaccard overlap helpers are added with unit coverage.
Hybrid recall and embedding backfill
src/memory/{mcp,search}.rs, src/cli/memory.rs, CHANGELOG.md
Remember indexes embeddings best-effort, recall combines FTS and vector candidates with decay and MMR, FTS-only fallback remains available, and a batched backfill command is added.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MemoryMCP
  participant EmbeddingEngine
  participant MemoryEmbeddings
  participant HybridSearch
  participant Retrieval
  MemoryMCP->>EmbeddingEngine: Embed document or query
  EmbeddingEngine-->>MemoryMCP: Return optional vector
  MemoryMCP->>MemoryEmbeddings: Upsert observation embedding
  MemoryMCP->>HybridSearch: Run hybrid recall
  HybridSearch->>MemoryEmbeddings: Fetch vector candidates
  HybridSearch->>Retrieval: Merge, decay, and rerank candidates
  Retrieval-->>MemoryMCP: Return ordered observations
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: migrating memory to the shared engine and adding hybrid recall.
Description check ✅ Passed It covers the summary and verification well, but it lacks the template's explicit Test plan and Notes for reviewers sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/188

Comment @coderabbitai help to get the list of available commands.

@getappz getappz changed the title store: migrate memory brain DB onto agentflare-store engine + hybrid recall feat(memory): migrate brain DB onto agentflare-store engine + hybrid recall Jul 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/memory/observations.rs (1)

244-245: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Enable foreign-key enforcement in every in-memory memory test database. These helpers migrate raw SQLite connections without PRAGMA foreign_keys = ON, so tests may not validate the PR's foreign-key enforcement contract.

  • src/memory/observations.rs#L244-L245: enable and assert the pragma before schema::migrate.
  • src/memory/mcp.rs#L404-L405: enable and assert the pragma before migration.
  • src/memory/relations.rs#L112-L113: enable and assert the pragma before migration.
  • src/memory/sessions.rs#L184-L185: enable and assert the pragma before migration.
  • src/memory/summaries.rs#L78-L79: enable and assert the pragma before migration.
src/memory/embeddings.rs (1)

41-70: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

candidates() doesn't filter by embedding model; mixed-model vectors would be compared directly.

observations_vec.model exists to track which model produced each vector (and the backfill CLI is designed to re-embed after a model change), but candidates()'s SQL never filters on it. If the embedding model is ever swapped and only some rows have been re-backfilled, cosine similarity would be computed between vectors from two different, semantically incompatible embedding spaces — silently degrading search_hybrid (src/memory/search.rs) recall quality with no error surfaced. Current tests only exercise a single, uniform model tag, so this isn't covered.

Consider filtering candidates() by the caller's current model name (threaded from engine::model_name()), so rows embedded with a stale model simply fall back to BM25-only ranking until re-backfilled.

♻️ Sketch of a model-aware filter
 pub fn candidates(
     conn: &Connection,
     query_vec: &[f32],
     project: Option<&str>,
     r#type: Option<&str>,
+    model: &str,
     k: usize,
 ) -> rusqlite::Result<Vec<(i64, f64)>> {
     let mut stmt = conn.prepare(
         "SELECT o.id, v.embedding
          FROM observations_vec v
          JOIN observations o ON o.id = v.obs_id
          WHERE o.deleted_at IS NULL
+           AND v.model = ?3
            AND (?1 IS NULL OR o.project = ?1)
            AND (?2 IS NULL OR o.type = ?2)",
     )?;
-    let rows = stmt.query_map(params![project, r#type], |r| {
+    let rows = stmt.query_map(params![project, r#type, model], |r| {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/memory/embeddings.rs` around lines 41 - 70, Update candidates() to accept
the caller’s current embedding model name and add an observations_vec.model
filter to its SQL parameters, excluding vectors tagged with stale models. Thread
engine::model_name() through the search_hybrid path into candidates(), while
preserving BM25-only fallback behavior when no current-model vectors are
available.
src/cli/memory.rs (1)

107-138: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Batch the backfill upserts into a single transaction.

Each embeddings::upsert call runs as its own implicit write transaction. For batch up to hundreds of rows, this means hundreds of separate commits (with associated WAL checkpoints) instead of one — wrap the loop in an explicit transaction and commit once at the end for a meaningful throughput win on larger backfills.

⚡ Sketch of batching the writes
                     Ok(todo) => {
                         let total = todo.len();
                         let mut ok = 0usize;
+                        let tx = conn.unchecked_transaction().ok();
                         for (id, text) in todo {
                             match crate::memory::engine::embed_doc(&text) {
                                 Some(vec) => {
                                     match crate::memory::embeddings::upsert(
                                         &conn, id, &vec, &model,
                                     ) {
                                         Ok(()) => ok += 1,
                                         Err(e) => eprintln!("obs {id}: store failed: {e}"),
                                     }
                                 }
                                 None => eprintln!("obs {id}: embed failed"),
                             }
                         }
+                        if let Some(tx) = tx {
+                            let _ = tx.commit();
+                        }
                         println!("backfilled {ok}/{total} embeddings (model: {model})");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/memory.rs` around lines 107 - 138, Update the
MemoryCommands::BackfillEmbeddings flow to open one explicit database
transaction before iterating over todo, perform each successful
embeddings::upsert through that transaction, and commit once after the loop
completes. Preserve the existing per-observation error reporting and success
count, and ensure transaction setup or final commit errors are surfaced through
the existing UI error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/memory/mcp.rs`:
- Around line 80-86: Update the search path centered on search::search_hybrid
and super::engine::embed_query to restrict hybrid candidates to the active
embedding model/version before scoring. Propagate the active model identifier
through the search arguments and filter observations_vec.model accordingly, or
enforce and validate a single-model invariant so vectors from mixed models are
never ranked together.
- Around line 44-53: Update the observation update path around embed_doc and
embeddings::upsert so a non-duplicate update removes or marks stale the existing
vector whenever embedding returns None or upsert fails. Preserve best-effort
remember behavior by not propagating the cleanup or indexing errors, while
leaving duplicate handling and successful upserts unchanged.

---

Nitpick comments:
In `@src/cli/memory.rs`:
- Around line 107-138: Update the MemoryCommands::BackfillEmbeddings flow to
open one explicit database transaction before iterating over todo, perform each
successful embeddings::upsert through that transaction, and commit once after
the loop completes. Preserve the existing per-observation error reporting and
success count, and ensure transaction setup or final commit errors are surfaced
through the existing UI error path.

In `@src/memory/embeddings.rs`:
- Around line 41-70: Update candidates() to accept the caller’s current
embedding model name and add an observations_vec.model filter to its SQL
parameters, excluding vectors tagged with stale models. Thread
engine::model_name() through the search_hybrid path into candidates(), while
preserving BM25-only fallback behavior when no current-model vectors are
available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 39f18faa-e2ce-42ad-9adf-571691dc407e

📥 Commits

Reviewing files that changed from the base of the PR and between a99cb02 and 1c1ae66.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • CHANGELOG.md
  • Cargo.toml
  • crates/agentflare-store/src/embed.rs
  • crates/agentflare-store/src/lib.rs
  • crates/agentflare-store/src/retrieval.rs
  • src/cli/memory.rs
  • src/memory/embeddings.rs
  • src/memory/engine.rs
  • src/memory/mcp.rs
  • src/memory/mod.rs
  • src/memory/observations.rs
  • src/memory/relations.rs
  • src/memory/schema.rs
  • src/memory/search.rs
  • src/memory/sessions.rs
  • src/memory/store.rs
  • src/memory/summaries.rs

Comment thread src/memory/mcp.rs
Comment thread src/memory/mcp.rs
Comment on lines +80 to +86
search::search_hybrid(
conn,
q,
input.project.as_deref(),
input.r#type.as_deref(),
limit,
super::engine::embed_query,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Filter hybrid candidates by embedding model.

search_hybrid receives only the query vector, while src/memory/embeddings.rs stores observations_vec.model but does not filter candidates by it. After a model change or mixed backfill, same-dimensional vectors from different models are scored together, producing invalid rankings. Pass the active model/version through the search path or enforce a single-model invariant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/memory/mcp.rs` around lines 80 - 86, Update the search path centered on
search::search_hybrid and super::engine::embed_query to restrict hybrid
candidates to the active embedding model/version before scoring. Propagate the
active model identifier through the search arguments and filter
observations_vec.model accordingly, or enforce and validate a single-model
invariant so vectors from mixed models are never ranked together.

@getappz

getappz commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner Author

Addressed CodeRabbit review (commit 7dc2e6ef):

  • Major — stale vector on failed reindex (mcp.rs): handle_remember now deletes the existing vector when an update can't be reindexed (no embedder / upsert failure), so recall never ranks on stale content and missing re-surfaces the row. Added embeddings::delete + roundtrip test.
  • Nitpick — FK enforcement in tests: enabled PRAGMA foreign_keys = ON in all memory in-memory test helpers + added a contract test (relation_to_missing_observation_is_rejected) that actually validates the FK-enforcement contract (original blocker A).

Deferred to item #202 (medium, under store epic #187): the Major mixed-model ranking finding + candidates() model filter + backfill-transaction perf nitpick. All three are inert in the shipping binary (the semantic feature is off by default → no vectors written); they only matter once semantic embeddings are enabled with a model change. #202 captures the single-model-invariant decision.

Gate re-run: fmt clean, clippy clean for authored code, 28/28 memory tests pass.

@getappz
getappz merged commit b0d0129 into master Jul 19, 2026
18 checks passed
@getappz
getappz deleted the task/188 branch July 19, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant