remem-ai 0.6.81

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use crate::memory::Memory;
use anyhow::{Context, Result};
use rusqlite::{params, Connection};

use super::discover::discover_entities;
use super::merge::rank_merged_ids;

fn make_memory(id: i64, title: &str, text: &str) -> Memory {
    Memory {
        id,
        session_id: None,
        project: "proj".to_string(),
        topic_key: None,
        title: title.to_string(),
        text: text.to_string(),
        memory_type: "discovery".to_string(),
        files: None,
        created_at_epoch: 0,
        updated_at_epoch: 0,
        status: "active".to_string(),
        branch: None,
        scope: "project".to_string(),
    }
}

#[test]
fn discover_entities_skips_query_entities_and_deduplicates() {
    let first_hop = vec![
        make_memory(1, "Melanie", "Tom Sarah"),
        make_memory(2, "Tom", "Sarah Tom"),
    ];

    let entities = discover_entities("Melanie", &first_hop);
    assert_eq!(entities, vec!["Tom", "Sarah"]);
}

#[test]
fn rank_merged_ids_boosts_overlap_and_respects_limit() {
    let ranked = rank_merged_ids(&[1, 2], &[2, 3], 3);
    assert_eq!(ranked, vec![2, 1, 3]);

    let limited = rank_merged_ids(&[1, 2], &[2, 3], 2);
    assert_eq!(limited, vec![2, 1]);
}

#[test]
fn search_multi_hop_demotes_verify_before_trust_second_hop() -> Result<()> {
    let conn = Connection::open_in_memory()?;
    crate::migrate::run_migrations(&conn)?;
    let first_id = crate::memory::insert_memory_full(
        &conn,
        Some("first-session"),
        "proj",
        Some("multi-hop-first"),
        "Melanie route",
        "Melanie mentions Tom in routing notes.",
        "discovery",
        None,
        Some("main"),
        "project",
        Some(150),
    )?;
    let stale_id = crate::memory::insert_memory_full(
        &conn,
        Some("stale-session"),
        "proj",
        Some("multi-hop-stale"),
        "Tom stale source",
        "Tom stale source-anchor memory.",
        "decision",
        Some(r#"["src/stale.rs"]"#),
        Some("main"),
        "project",
        Some(300),
    )?;
    let fresh_id = crate::memory::insert_memory_full(
        &conn,
        Some("fresh-session"),
        "proj",
        Some("multi-hop-fresh"),
        "Tom fresh source",
        "Tom fresh source-anchor memory.",
        "decision",
        Some(r#"["src/fresh.rs"]"#),
        Some("main"),
        "project",
        Some(150),
    )?;
    crate::retrieval::entity::link_entities(&conn, stale_id, &["Tom".to_string()])?;
    crate::retrieval::entity::link_entities(&conn, fresh_id, &["Tom".to_string()])?;
    link_commit(
        &conn,
        1,
        "source-stale",
        100,
        &["src/stale.rs"],
        "stale-session",
    )?;
    insert_commit(&conn, 2, "later-stale", 200, &["src/stale.rs"])?;
    link_commit(
        &conn,
        3,
        "source-fresh",
        100,
        &["src/fresh.rs"],
        "fresh-session",
    )?;

    let result = super::search::search_multi_hop(
        &conn,
        "Melanie",
        Some("proj"),
        3,
        0,
        None,
        Some("main"),
        false,
        false,
    )?;
    let ids = result
        .memories
        .iter()
        .map(|memory| memory.id)
        .collect::<Vec<_>>();

    assert_eq!(result.hops, 2);
    assert_eq!(ids.first().copied(), Some(first_id));
    let fresh_rank = ids
        .iter()
        .position(|id| *id == fresh_id)
        .context("fresh second-hop memory missing")?;
    let stale_rank = ids
        .iter()
        .position(|id| *id == stale_id)
        .context("stale second-hop memory missing")?;
    assert!(fresh_rank < stale_rank, "{ids:?}");
    Ok(())
}

fn link_commit(
    conn: &Connection,
    id: i64,
    sha: &str,
    epoch: i64,
    changed_files: &[&str],
    memory_session_id: &str,
) -> Result<()> {
    insert_commit(conn, id, sha, epoch, changed_files)?;
    conn.execute(
        "INSERT INTO git_commit_sessions
         (commit_id, session_id, memory_session_id, source, linked_at_epoch)
         VALUES (?1, ?2, ?3, 'test', ?4)",
        params![id, format!("content-{id}"), memory_session_id, epoch],
    )?;
    Ok(())
}

fn insert_commit(
    conn: &Connection,
    id: i64,
    sha: &str,
    epoch: i64,
    changed_files: &[&str],
) -> Result<()> {
    let changed_files = serde_json::to_string(changed_files)?;
    conn.execute(
        "INSERT INTO git_commits
         (id, project, repo_path, sha, short_sha, branch, message,
          authored_at_epoch, changed_files, created_at_epoch, updated_at_epoch)
         VALUES (?1, 'proj', '/repo', ?2, ?2, 'main', NULL, ?3, ?4, ?3, ?3)",
        params![id, sha, epoch, changed_files],
    )?;
    Ok(())
}