remem-ai 0.5.11

Persistent memory for Claude Code and Codex — single binary, automatic context
Documentation
use anyhow::Result;
use rusqlite::params;

use super::{
    insert_source_observation, low_risk_candidate_xml, process_with_generator, setup_conn,
    setup_task, MemoryCandidateResult,
};

#[tokio::test]
async fn memory_candidate_existing_same_topic_same_text_becomes_noop() -> Result<()> {
    let mut conn = setup_conn();
    let task = setup_task(&mut conn, "sess-candidate-noop")?;
    let text = "Use the worker loop to process extraction tasks after observation extraction.";
    insert_source_observation(&conn, &task, text)?;
    crate::memory::insert_memory_full(
        &conn,
        None,
        "/tmp/remem",
        Some("decision-worker-loop"),
        "Existing worker loop",
        text,
        "decision",
        None,
        None,
        "project",
        None,
    )?;

    let result = process_with_generator(&mut conn, &task, |_prompt| async {
        Ok(low_risk_candidate_xml())
    })
    .await?;

    assert_eq!(
        result,
        MemoryCandidateResult::Written {
            candidates: 1,
            promoted: 0,
            pending_review: 0
        }
    );
    let review_status: String =
        conn.query_row("SELECT review_status FROM memory_candidates", [], |row| {
            row.get(0)
        })?;
    let memory_count: i64 =
        conn.query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))?;
    assert_eq!(review_status, "noop");
    assert_eq!(memory_count, 1);
    let (operation, source_candidate_id, noop_reason): (String, Option<i64>, Option<String>) = conn
        .query_row(
            "SELECT operation, source_candidate_id, noop_reason
             FROM memory_operation_log
             ORDER BY id DESC
             LIMIT 1",
            [],
            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
        )?;
    let candidate_id: i64 =
        conn.query_row("SELECT id FROM memory_candidates", [], |row| row.get(0))?;
    assert_eq!(operation, "noop");
    assert_eq!(source_candidate_id, Some(candidate_id));
    assert_eq!(
        noop_reason.as_deref(),
        Some("already represented by active memory")
    );
    let edge_count: i64 =
        conn.query_row("SELECT COUNT(*) FROM memory_edges", [], |row| row.get(0))?;
    assert_eq!(edge_count, 0);
    Ok(())
}

#[tokio::test]
async fn memory_candidate_newer_same_topic_supersedes_old_memory() -> Result<()> {
    let mut conn = setup_conn();
    let task = setup_task(&mut conn, "sess-candidate-update")?;
    let text =
        "Use the async worker loop to process extraction tasks after observation extraction.";
    insert_source_observation(&conn, &task, text)?;
    let old_id = crate::memory::insert_memory_full(
        &conn,
        None,
        "/tmp/remem",
        Some("decision-worker-loop"),
        "Old worker loop",
        "Use the synchronous worker loop to process extraction tasks after observation extraction.",
        "decision",
        None,
        None,
        "project",
        None,
    )?;

    let result = process_with_generator(&mut conn, &task, |_prompt| async {
        Ok(format!(
            "<memory_candidate>\
                <scope>project</scope>\
                <type>decision</type>\
                <topic_key>decision-worker-loop</topic_key>\
                <risk_class>low</risk_class>\
                <confidence>0.92</confidence>\
                <text>{text}</text>\
             </memory_candidate>"
        ))
    })
    .await?;

    assert_eq!(
        result,
        MemoryCandidateResult::Written {
            candidates: 1,
            promoted: 1,
            pending_review: 0
        }
    );
    let old_status: String = conn.query_row(
        "SELECT status FROM memories WHERE id = ?1",
        params![old_id],
        |row| row.get(0),
    )?;
    let (new_id, active_text, candidate_id, candidate_evidence): (i64, String, i64, String) = conn
        .query_row(
            "SELECT m.id, m.content, c.id, c.evidence_event_ids
             FROM memories m
             JOIN memory_candidates c ON c.id = m.source_candidate_id
             WHERE m.topic_key = 'decision-worker-loop' AND m.status = 'active'",
            [],
            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
        )?;
    let memory_count: i64 =
        conn.query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))?;
    assert_eq!(old_status, "stale");
    assert_eq!(active_text, text);
    assert_eq!(memory_count, 2);
    let (operation_id, operation, superseded_ids): (i64, String, String) = conn.query_row(
        "SELECT id, operation, superseded_ids
         FROM memory_operation_log
         ORDER BY id DESC
         LIMIT 1",
        [],
        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
    )?;
    assert_eq!(operation, "update");
    assert_eq!(
        serde_json::from_str::<Vec<i64>>(&superseded_ids)?,
        vec![old_id]
    );

    let supersedes_edge: (String, i64, i64, Option<i64>, String, Option<i64>) = conn.query_row(
        "SELECT edge_type, from_memory_id, to_memory_id, source_candidate_id,
                evidence_event_ids, source_operation_id
         FROM memory_edges
         WHERE edge_type = 'supersedes'
           AND from_memory_id = ?1
           AND to_memory_id = ?2",
        params![old_id, new_id],
        |row| {
            Ok((
                row.get(0)?,
                row.get(1)?,
                row.get(2)?,
                row.get(3)?,
                row.get(4)?,
                row.get(5)?,
            ))
        },
    )?;
    assert_eq!(supersedes_edge.0, "supersedes");
    assert_eq!(supersedes_edge.1, old_id);
    assert_eq!(supersedes_edge.2, new_id);
    assert_eq!(supersedes_edge.3, Some(candidate_id));
    assert_eq!(supersedes_edge.5, Some(operation_id));
    assert_eq!(
        serde_json::from_str::<Vec<i64>>(&supersedes_edge.4)?,
        serde_json::from_str::<Vec<i64>>(&candidate_evidence)?
    );
    let derived_count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM memory_edges
         WHERE edge_type = 'derived_from'
           AND from_memory_id IS NULL
           AND to_memory_id = ?1
           AND source_candidate_id = ?2
           AND source_operation_id = ?3",
        params![new_id, candidate_id, operation_id],
        |row| row.get(0),
    )?;
    assert_eq!(derived_count, 1);
    Ok(())
}