remem-ai 0.5.96

Persistent memory for Claude Code and OpenAI Codex coding agents
Documentation
use anyhow::Result;
use rusqlite::{params, Connection};

use super::{run_migrations, MIGRATIONS};

fn setup_v040_db() -> Result<Connection> {
    let conn = Connection::open_in_memory()?;
    conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;

    for migration in MIGRATIONS
        .iter()
        .filter(|migration| migration.version <= 40)
    {
        conn.execute_batch(migration.sql)?;
    }
    conn.execute_batch(
        "CREATE TABLE _schema_migrations (
            version INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            applied_at_epoch INTEGER NOT NULL
        );",
    )?;
    for migration in MIGRATIONS
        .iter()
        .filter(|migration| migration.version <= 40)
    {
        conn.execute(
            "INSERT INTO _schema_migrations (version, name, applied_at_epoch)
             VALUES (?1, ?2, ?3)",
            params![migration.version, migration.name, 1_700_000_000_i64],
        )?;
    }
    Ok(conn)
}

#[test]
fn content_identity_migration_backfills_legacy_hashes() -> Result<()> {
    let conn = setup_v040_db()?;
    let raw_content = "legacy searchable raw message";
    conn.execute(
        "INSERT INTO raw_messages
         (session_id, project, role, content, content_hash, source, created_at_epoch)
         VALUES ('session-a', '/tmp/remem-content-id', 'user', ?1, ?2, 'hook', 100)",
        params![
            raw_content,
            crate::db::legacy_content_identity_hash(raw_content.as_bytes())
        ],
    )?;

    let now = 1_700_000_000_i64;
    let host_id: i64 =
        conn.query_row("SELECT id FROM hosts WHERE name = 'codex-cli'", [], |row| {
            row.get(0)
        })?;
    conn.execute(
        "INSERT INTO workspaces(root_path, created_at_epoch, updated_at_epoch)
         VALUES ('/tmp/remem-content-id', ?1, ?1)",
        [now],
    )?;
    let workspace_id = conn.last_insert_rowid();
    conn.execute(
        "INSERT INTO projects(workspace_id, project_path, project_key, created_at_epoch, updated_at_epoch)
         VALUES (?1, '/tmp/remem-content-id', 'tmp-remem-content-id', ?2, ?2)",
        params![workspace_id, now],
    )?;
    let project_id = conn.last_insert_rowid();
    conn.execute(
        "INSERT INTO sessions(host_id, workspace_id, project_id, session_id, last_seen_at_epoch, status)
         VALUES (?1, ?2, ?3, 'session-a', ?4, 'active')",
        params![host_id, workspace_id, project_id, now],
    )?;
    let session_row_id = conn.last_insert_rowid();

    let inline_content = "legacy inline captured content";
    conn.execute(
        "INSERT INTO captured_events
         (host_id, workspace_id, project_id, session_row_id, session_id, event_id,
          event_type, content_text, content_hash, token_estimate, retention_class,
          created_at_epoch, inserted_at_epoch)
         VALUES (?1, ?2, ?3, ?4, 'session-a', 'inline-event', 'message',
                 ?5, ?6, 1, 'raw_keep', ?7, ?7)",
        params![
            host_id,
            workspace_id,
            project_id,
            session_row_id,
            inline_content,
            crate::db::legacy_content_identity_hash(inline_content.as_bytes()),
            now
        ],
    )?;

    let blob_bytes = b"legacy full blob bytes for v041";
    conn.execute(
        "INSERT INTO event_blobs
         (content_hash, content_encoding, content_bytes, original_bytes, stored_bytes, created_at_epoch)
         VALUES (?1, 'plain', ?2, ?3, ?3, ?4)",
        params![
            crate::db::legacy_content_identity_hash(blob_bytes),
            blob_bytes,
            blob_bytes.len() as i64,
            now
        ],
    )?;
    let blob_id = conn.last_insert_rowid();
    conn.execute(
        "INSERT INTO captured_events
         (host_id, workspace_id, project_id, session_row_id, session_id, event_id,
          event_type, content_text, content_blob_id, content_hash, token_estimate,
          retention_class, created_at_epoch, inserted_at_epoch)
         VALUES (?1, ?2, ?3, ?4, 'session-a', 'blob-event', 'tool_result',
                 'compact preview should not be hashed', ?5, ?6, 10, 'raw_compact', ?7, ?7)",
        params![
            host_id,
            workspace_id,
            project_id,
            session_row_id,
            blob_id,
            crate::db::legacy_content_identity_hash(blob_bytes),
            now
        ],
    )?;

    run_migrations(&conn)?;

    let raw_hash: String = conn.query_row("SELECT content_hash FROM raw_messages", [], |row| {
        row.get(0)
    })?;
    let blob_hash: String =
        conn.query_row("SELECT content_hash FROM event_blobs", [], |row| row.get(0))?;
    let inline_hash: String = conn.query_row(
        "SELECT content_hash FROM captured_events WHERE event_id = 'inline-event'",
        [],
        |row| row.get(0),
    )?;
    let blob_event_hash: String = conn.query_row(
        "SELECT content_hash FROM captured_events WHERE event_id = 'blob-event'",
        [],
        |row| row.get(0),
    )?;
    let fts_hits: i64 = conn.query_row(
        "SELECT COUNT(*) FROM raw_messages_fts WHERE raw_messages_fts MATCH 'searchable'",
        [],
        |row| row.get(0),
    )?;

    assert_eq!(
        raw_hash,
        crate::db::content_identity_hash(raw_content.as_bytes())
    );
    assert_eq!(blob_hash, crate::db::content_identity_hash(blob_bytes));
    assert_eq!(
        inline_hash,
        crate::db::content_identity_hash(inline_content.as_bytes())
    );
    assert_eq!(
        blob_event_hash,
        crate::db::content_identity_hash(blob_bytes)
    );
    assert_eq!(fts_hits, 1);
    Ok(())
}

#[test]
fn content_identity_migration_collapses_mixed_raw_duplicates() -> Result<()> {
    let conn = setup_v040_db()?;
    let content = "mixed legacy and sha duplicate raw message";
    let legacy_hash = crate::db::legacy_content_identity_hash(content.as_bytes());
    let sha_hash = crate::db::content_identity_hash(content.as_bytes());

    conn.execute(
        "INSERT INTO raw_messages
         (id, session_id, project, role, content, content_hash, source, created_at_epoch)
         VALUES (1, 'session-a', '/tmp/remem-content-id', 'user', ?1, ?2, 'hook', 100)",
        params![content, legacy_hash],
    )?;
    conn.execute(
        "INSERT INTO raw_messages
         (id, session_id, project, role, content, content_hash, source, created_at_epoch)
         VALUES (2, 'session-a', '/tmp/remem-content-id', 'user', ?1, ?2, 'hook', 101)",
        params![content, sha_hash],
    )?;

    run_migrations(&conn)?;

    let rows: Vec<(i64, String)> = {
        let mut stmt = conn.prepare("SELECT id, content_hash FROM raw_messages ORDER BY id")?;
        let mapped = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
        let mut rows = Vec::new();
        for row in mapped {
            rows.push(row?);
        }
        rows
    };
    let fts_hits: i64 = conn.query_row(
        "SELECT COUNT(*) FROM raw_messages_fts WHERE raw_messages_fts MATCH 'duplicate'",
        [],
        |row| row.get(0),
    )?;

    assert_eq!(rows, vec![(1, sha_hash)]);
    assert_eq!(fts_hits, 1);
    Ok(())
}