remem-ai 0.6.70

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::collections::HashMap;

use anyhow::Result;
use rusqlite::Connection;

use crate::memory::{self, Memory};

const STALE_SCORE_FACTOR: f64 = 0.25;

pub(crate) fn apply_score_demotions(
    conn: &Connection,
    fused: &[(i64, f64)],
    ordered: Vec<Memory>,
) -> Result<(Vec<Memory>, Vec<(i64, f64)>)> {
    if fused.is_empty() || ordered.is_empty() {
        return Ok((ordered, fused.to_vec()));
    }
    let now_epoch = chrono::Utc::now().timestamp();
    let labels = memory::staleness::memory_staleness_labels_for_memories_lossy(
        conn,
        &ordered,
        now_epoch,
        |id, error| {
            crate::log::warn(
                "retrieval",
                &format!("source-anchor label fallback for memory {id}: {error}"),
            );
        },
    )
    .unwrap_or_else(|error| {
        crate::log::warn(
            "retrieval",
            &format!("source-anchor batch fallback: {error}"),
        );
        ordered
            .iter()
            .map(|memory| (memory.id, memory::memory_staleness_label(memory, now_epoch)))
            .collect()
    });
    let original_rank = fused
        .iter()
        .enumerate()
        .map(|(rank, (id, _))| (*id, rank))
        .collect::<HashMap<_, _>>();
    let mut demoted = fused
        .iter()
        .map(|(id, score)| {
            let score = if labels
                .get(id)
                .is_some_and(|label| label.source_anchor == "verify-before-trust")
            {
                score * STALE_SCORE_FACTOR
            } else {
                *score
            };
            (*id, score)
        })
        .collect::<Vec<_>>();
    demoted.sort_by(|left, right| {
        right
            .1
            .partial_cmp(&left.1)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then_with(|| {
                original_rank
                    .get(&left.0)
                    .copied()
                    .unwrap_or(usize::MAX)
                    .cmp(&original_rank.get(&right.0).copied().unwrap_or(usize::MAX))
            })
    });
    let id_to_memory = ordered
        .into_iter()
        .map(|memory| (memory.id, memory))
        .collect::<HashMap<_, _>>();
    let ordered = demoted
        .iter()
        .filter_map(|(id, _)| id_to_memory.get(id).cloned())
        .collect::<Vec<_>>();
    Ok((ordered, demoted))
}

#[cfg(test)]
mod tests {
    use super::*;
    use rusqlite::params;

    #[test]
    fn apply_score_demotions_ranks_verify_before_trust_lower() -> anyhow::Result<()> {
        let conn = Connection::open_in_memory()?;
        crate::migrate::run_migrations(&conn)?;
        seed_memory(&conn, 1, "stale-session", r#"["src/stale.rs"]"#)?;
        seed_memory(&conn, 2, "fresh-session", r#"["src/fresh.rs"]"#)?;
        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 ordered = memory::get_memories_by_ids(&conn, &[1, 2], None)?;
        let fused = vec![(1, 1.0), (2, 0.9)];

        let (ordered, demoted) = apply_score_demotions(&conn, &fused, ordered)?;

        assert_eq!(
            ordered.iter().map(|memory| memory.id).collect::<Vec<_>>(),
            vec![2, 1]
        );
        assert_eq!(demoted[0].0, 2);
        assert!(demoted[1].1 < demoted[0].1);
        Ok(())
    }

    #[test]
    fn apply_score_demotions_keeps_search_results_when_anchor_labels_fail() -> anyhow::Result<()> {
        let conn = Connection::open_in_memory()?;
        crate::migrate::run_migrations(&conn)?;
        seed_memory(&conn, 1, "malformed-session", "[not-json")?;
        seed_memory(&conn, 2, "fresh-session", r#"["src/fresh.rs"]"#)?;

        let ordered = memory::get_memories_by_ids(&conn, &[1, 2], None)?;
        let fused = vec![(1, 1.0), (2, 0.9)];

        let (ordered, demoted) = apply_score_demotions(&conn, &fused, ordered)?;

        assert_eq!(
            ordered.iter().map(|memory| memory.id).collect::<Vec<_>>(),
            vec![1, 2]
        );
        assert_eq!(demoted, fused);
        Ok(())
    }

    #[test]
    fn apply_score_demotions_keeps_search_results_when_commit_files_fail() -> anyhow::Result<()> {
        let conn = Connection::open_in_memory()?;
        crate::migrate::run_migrations(&conn)?;
        use_legacy_git_commit_lookup(&conn)?;
        seed_memory(&conn, 1, "malformed-session", r#"["src/stale.rs"]"#)?;
        seed_memory(&conn, 2, "fresh-session", r#"["src/fresh.rs"]"#)?;
        link_commit(
            &conn,
            1,
            "source-malformed",
            100,
            &["src/stale.rs"],
            "malformed-session",
        )?;
        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 (2, 'proj', '/repo', 'later-malformed', 'later-malformed', 'main',
                     NULL, 200, '[not-json', 200, 200)",
            [],
        )?;

        let ordered = memory::get_memories_by_ids(&conn, &[1, 2], None)?;
        let fused = vec![(1, 1.0), (2, 0.9)];

        let (ordered, demoted) = apply_score_demotions(&conn, &fused, ordered)?;

        assert_eq!(
            ordered.iter().map(|memory| memory.id).collect::<Vec<_>>(),
            vec![1, 2]
        );
        assert_eq!(demoted, fused);
        Ok(())
    }

    fn use_legacy_git_commit_lookup(conn: &Connection) -> anyhow::Result<()> {
        conn.execute_batch(
            "DROP TRIGGER git_commits_validate_changed_files_insert;
             DROP TRIGGER git_commits_validate_changed_files_update;
             DROP TRIGGER git_commits_sync_files_insert;
             DROP TRIGGER git_commits_sync_files_update;
             DROP TRIGGER git_commits_sync_files_delete;
             DROP TABLE git_commit_files;
             DROP INDEX idx_git_commits_project_commit_epoch;",
        )?;
        Ok(())
    }

    fn seed_memory(
        conn: &Connection,
        id: i64,
        session_id: &str,
        files: &str,
    ) -> anyhow::Result<()> {
        conn.execute(
            "INSERT INTO memories
             (id, session_id, project, topic_key, title, content, memory_type, files,
              created_at_epoch, updated_at_epoch, status, branch, scope)
             VALUES (?1, ?2, 'proj', ?3, ?4, ?5, 'decision', ?6, 100, 100,
                     'active', 'main', 'project')",
            params![
                id,
                session_id,
                format!("topic-{id}"),
                format!("Memory {id}"),
                format!("Memory {id} content"),
                files
            ],
        )?;
        Ok(())
    }

    fn link_commit(
        conn: &Connection,
        id: i64,
        sha: &str,
        epoch: i64,
        changed_files: &[&str],
        memory_session_id: &str,
    ) -> anyhow::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],
    ) -> anyhow::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(())
    }
}