remem-ai 0.6.36

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

use crate::memory;

use super::{query_global_preferences, query_project_preferences};

pub fn list_preferences(conn: &Connection, project: &str) -> Result<()> {
    let project_prefs = query_project_preferences(conn, project, 50)?;
    let global_prefs = query_global_preferences(conn, 10).unwrap_or_default();

    if project_prefs.is_empty() && global_prefs.is_empty() {
        println!("No preferences found.");
        return Ok(());
    }

    if !project_prefs.is_empty() {
        println!("Project preferences ({}):", project);
        for pref in &project_prefs {
            let text_preview: String = pref.text.chars().take(80).collect();
            println!("  [{}] {}", pref.id, text_preview);
        }
    }

    if !global_prefs.is_empty() {
        println!("\nGlobal preferences:");
        for pref in &global_prefs {
            let text_preview: String = pref.text.chars().take(80).collect();
            println!("  [{}] {} (from: {})", pref.id, text_preview, pref.project);
        }
    }

    Ok(())
}

pub fn add_preference(conn: &Connection, project: &str, text: &str, global: bool) -> Result<i64> {
    let title = format!("Preference: {}", &text[..text.len().min(60)]);
    let topic_key = format!(
        "manual-preference-{}",
        crate::memory::slugify_for_topic(text, 50)
    );
    let scope = if global { "global" } else { "project" };
    let tx = conn.unchecked_transaction()?;
    let mut stmt = tx.prepare(
        "SELECT m.id, m.content,
                EXISTS(
                    SELECT 1 FROM memory_preference_reinforcements r WHERE r.memory_id = m.id
                )
         FROM memories m
         WHERE m.memory_type = 'preference'",
    )?;
    let rows = stmt.query_map([], |row| {
        Ok((
            row.get::<_, i64>(0)?,
            row.get::<_, String>(1)?,
            row.get::<_, bool>(2)?,
        ))
    })?;
    let previous_preferences = crate::db::query::collect_rows(rows)?;
    drop(stmt);
    let id = memory::insert_memory_full(
        &tx,
        None,
        project,
        Some(&topic_key),
        &title,
        text,
        "preference",
        None,
        None,
        scope,
        None,
    )?;
    if let Some((_, previous_text, _)) = previous_preferences
        .iter()
        .find(|(memory_id, _, had_rule_state)| *memory_id == id && *had_rule_state)
    {
        crate::memory::preference::compilation::enqueue_for_memory_ids(&tx, &[id])?;
        crate::memory::preference::reinforcement::reconcile_in_place_preference_update(
            &tx,
            id,
            previous_text,
            text,
        )?;
    }
    tx.commit()?;
    Ok(id)
}

pub fn remove_preference(conn: &Connection, id: i64) -> Result<bool> {
    let tx = conn.unchecked_transaction()?;
    crate::memory::preference::compilation::enqueue_for_memory_ids(&tx, &[id])?;
    let count = tx.execute(
        "UPDATE memories SET status = 'archived' WHERE id = ?1 AND memory_type = 'preference'",
        params![id],
    )?;
    tx.commit()?;
    Ok(count > 0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::db::test_support::ScopedTestDataDir;

    #[test]
    fn cli_add_refinement_reconciles_candidate_rule_state_and_enqueues_compile() -> Result<()> {
        let _dir = ScopedTestDataDir::new("preference-cli-add-rule-reconciliation");
        crate::runtime_config::init_config()?;
        crate::runtime_config::set_config_value("rule_compilation.enabled", "true")?;
        let conn = crate::db::open_db()?;
        let original = "Do not add AI-generated-by or Co-authored-by trailers to commits";
        let refinement = "Do not add Co-authored-by trailers to commits";
        let id = add_preference(&conn, "/repo", original, false)?;
        conn.execute(
            "INSERT INTO memory_candidates
             (id, scope, memory_type, topic_key, text, evidence_event_ids,
              confidence, risk_class, review_status, created_at_epoch, updated_at_epoch,
              source_trust_class)
             VALUES (900, 'project', 'preference', 'commit-trailers', ?1, '[1,2,3]',
                     0.95, 'low', 'approved', 1, 3, 'user_prompt')",
            [original],
        )?;
        conn.execute(
            "UPDATE memories
             SET source_candidate_id = 900, source_trust_class = 'user_prompt'
             WHERE id = ?1",
            [id],
        )?;
        conn.execute(
            "INSERT INTO memory_preference_reinforcements
             (memory_id, reinforcement_count, source_evidence,
              last_reinforced_at_epoch, created_at_epoch, updated_at_epoch,
              machine_checkable, risk_class)
             VALUES (?1, 3, '[1,2,3]', 3, 1, 3, 1, 'low')",
            [id],
        )?;
        conn.execute(
            "INSERT INTO preference_rule_overrides
             (project, rule_id, source_memory_id, disabled, action_override,
              updated_by, updated_at_epoch)
             VALUES ('/repo', ?1, ?2, 1, 'block', 'user', 4)",
            params![format!("pref-{id}-1"), id],
        )?;

        let refined_id = add_preference(&conn, "/repo", refinement, false)?;

        assert_eq!(
            refined_id, id,
            "CLI refinement should reuse the canonical row"
        );
        let state: (String, Option<i64>) = conn.query_row(
            "SELECT content, source_candidate_id FROM memories WHERE id = ?1",
            [id],
            |row| Ok((row.get(0)?, row.get(1)?)),
        )?;
        assert_eq!(state, (refinement.to_string(), None));
        let reinforcement_rows: i64 = conn.query_row(
            "SELECT COUNT(*) FROM memory_preference_reinforcements WHERE memory_id = ?1",
            [id],
            |row| row.get(0),
        )?;
        assert_eq!(reinforcement_rows, 0);
        let override_rows: i64 = conn.query_row(
            "SELECT COUNT(*) FROM preference_rule_overrides WHERE source_memory_id = ?1",
            [id],
            |row| row.get(0),
        )?;
        assert_eq!(override_rows, 0);
        let pending: i64 = conn.query_row(
            "SELECT COUNT(*) FROM jobs
             WHERE job_type = 'compile_rules' AND project = '/repo' AND state = 'pending'",
            [],
            |row| row.get(0),
        )?;
        assert_eq!(pending, 1);
        Ok(())
    }
}