remem-ai 0.6.62

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use super::{column_exists, ImportedMarkdownMemory, MarkdownMemoryFactMetadata};
use anyhow::{Context, Result};
use rusqlite::Connection;
use std::collections::{BTreeMap, HashSet};

pub(super) fn load_markdown_memory_facts(
    conn: &Connection,
    memory_id: i64,
) -> Result<Vec<MarkdownMemoryFactMetadata>> {
    if !column_exists(conn, "memory_facts", "source_memory_id")? {
        return Ok(Vec::new());
    }
    let invalidated_expr = if column_exists(conn, "memory_facts", "invalidated_at_epoch")? {
        "invalidated_at_epoch"
    } else {
        "NULL AS invalidated_at_epoch"
    };
    let sql = format!(
        "SELECT id, project, subject, predicate, object, valid_from_epoch,
                valid_to_epoch, learned_at_epoch, source_observation_id,
                source_event_ids, confidence, supersedes_fact_id, status,
                created_at_epoch, updated_at_epoch, {invalidated_expr}
         FROM memory_facts
         WHERE source_memory_id = ?1
         ORDER BY id"
    );
    let mut stmt = conn.prepare(&sql)?;
    let rows = stmt.query_map([memory_id], |row| {
        let source_event_json: String = row.get(9)?;
        let source_event_ids =
            serde_json::from_str::<Vec<i64>>(&source_event_json).map_err(|err| {
                rusqlite::Error::FromSqlConversionFailure(
                    9,
                    rusqlite::types::Type::Text,
                    Box::new(err),
                )
            })?;
        Ok(MarkdownMemoryFactMetadata {
            source_id: row.get(0)?,
            project: row.get(1)?,
            subject: row.get(2)?,
            predicate: row.get(3)?,
            object: row.get(4)?,
            valid_from_epoch: row.get(5)?,
            valid_to_epoch: row.get(6)?,
            learned_at_epoch: row.get(7)?,
            source_observation_id: row.get(8)?,
            source_event_ids,
            confidence: row.get(10)?,
            supersedes_fact_id: row.get(11)?,
            status: row.get(12)?,
            created_at_epoch: row.get(13)?,
            updated_at_epoch: row.get(14)?,
            invalidated_at_epoch: row.get(15)?,
        })
    })?;
    crate::db::query::collect_rows(rows)
        .with_context(|| format!("load markdown memory facts for memory id={memory_id}"))
}

pub(super) fn replace_markdown_memory_facts(
    conn: &Connection,
    imported: &[ImportedMarkdownMemory],
) -> Result<()> {
    let has_fact_metadata = imported
        .iter()
        .any(|memory| memory.doc.metadata.facts.is_some());
    if !has_fact_metadata {
        return Ok(());
    }
    let has_nonempty_facts = imported.iter().any(|memory| {
        memory
            .doc
            .metadata
            .facts
            .as_ref()
            .is_some_and(|facts| !facts.is_empty())
    });
    if !column_exists(conn, "memory_facts", "source_memory_id")? {
        if !has_nonempty_facts {
            return Ok(());
        }
        anyhow::bail!(
            "markdown archive contains memory_facts but target database lacks memory_facts table"
        );
    }

    conn.execute_batch("SAVEPOINT remem_restore_markdown_facts")?;
    let result = (|| -> Result<()> {
        let mut replaced_memory_ids = HashSet::new();
        for memory in imported
            .iter()
            .filter(|memory| memory.doc.metadata.facts.is_some())
        {
            if replaced_memory_ids.insert(memory.memory_id) {
                conn.execute(
                    "DELETE FROM memory_facts WHERE source_memory_id = ?1",
                    [memory.memory_id],
                )?;
            }
        }

        let has_invalidated_at_epoch = column_exists(conn, "memory_facts", "invalidated_at_epoch")?;
        let mut remapped_ids = BTreeMap::new();
        for memory in imported {
            let Some(facts) = memory.doc.metadata.facts.as_ref() else {
                continue;
            };
            for fact in facts {
                let new_id =
                    insert_markdown_fact(conn, memory.memory_id, fact, has_invalidated_at_epoch)?;
                if let Some(source_id) = fact.source_id {
                    remapped_ids.insert(source_id, new_id);
                }
            }
        }
        for memory in imported {
            let Some(facts) = memory.doc.metadata.facts.as_ref() else {
                continue;
            };
            for fact in facts {
                let Some(source_id) = fact.source_id else {
                    continue;
                };
                let Some(new_id) = remapped_ids.get(&source_id).copied() else {
                    continue;
                };
                let supersedes_fact_id = fact
                    .supersedes_fact_id
                    .and_then(|old_id| remapped_ids.get(&old_id).copied());
                conn.execute(
                    "UPDATE memory_facts SET supersedes_fact_id = ?1 WHERE id = ?2",
                    rusqlite::params![supersedes_fact_id, new_id],
                )?;
            }
        }
        Ok(())
    })();

    match result {
        Ok(()) => {
            conn.execute_batch("RELEASE SAVEPOINT remem_restore_markdown_facts")?;
            Ok(())
        }
        Err(error) => {
            if let Err(rollback_error) = conn.execute_batch(
                "ROLLBACK TO SAVEPOINT remem_restore_markdown_facts; RELEASE SAVEPOINT remem_restore_markdown_facts",
            ) {
                return Err(rollback_error)
                    .context(format!("rollback markdown fact restore after failure: {error}"));
            }
            Err(error)
        }
    }
}

fn insert_markdown_fact(
    conn: &Connection,
    memory_id: i64,
    fact: &MarkdownMemoryFactMetadata,
    has_invalidated_at_epoch: bool,
) -> Result<i64> {
    validate_fact(fact)?;
    let source_event_ids = serde_json::to_string(&Vec::<i64>::new())?;
    if has_invalidated_at_epoch {
        conn.execute(
            "INSERT INTO memory_facts
             (project, subject, predicate, object, valid_from_epoch, valid_to_epoch,
              learned_at_epoch, source_memory_id, source_observation_id, source_event_ids,
              confidence, supersedes_fact_id, status, created_at_epoch, updated_at_epoch,
              invalidated_at_epoch)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, NULL, ?12, ?13, ?14, ?15)",
            rusqlite::params![
                fact.project,
                fact.subject,
                fact.predicate,
                fact.object,
                fact.valid_from_epoch,
                fact.valid_to_epoch,
                fact.learned_at_epoch,
                memory_id,
                Option::<i64>::None,
                source_event_ids,
                fact.confidence,
                fact.status,
                fact.created_at_epoch,
                fact.updated_at_epoch,
                fact.invalidated_at_epoch,
            ],
        )?;
    } else {
        conn.execute(
            "INSERT INTO memory_facts
             (project, subject, predicate, object, valid_from_epoch, valid_to_epoch,
              learned_at_epoch, source_memory_id, source_observation_id, source_event_ids,
              confidence, supersedes_fact_id, status, created_at_epoch, updated_at_epoch)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, NULL, ?12, ?13, ?14)",
            rusqlite::params![
                fact.project,
                fact.subject,
                fact.predicate,
                fact.object,
                fact.valid_from_epoch,
                fact.valid_to_epoch,
                fact.learned_at_epoch,
                memory_id,
                Option::<i64>::None,
                source_event_ids,
                fact.confidence,
                fact.status,
                fact.created_at_epoch,
                fact.updated_at_epoch,
            ],
        )?;
    }
    Ok(conn.last_insert_rowid())
}

fn validate_fact(fact: &MarkdownMemoryFactMetadata) -> Result<()> {
    if fact.project.trim().is_empty() {
        anyhow::bail!("markdown memory fact project must not be empty");
    }
    if fact.subject.trim().is_empty() {
        anyhow::bail!("markdown memory fact subject must not be empty");
    }
    if fact.object.trim().is_empty() {
        anyhow::bail!("markdown memory fact object must not be empty");
    }
    if !is_supported_fact_predicate(&fact.predicate) {
        anyhow::bail!(
            "unsupported markdown memory fact predicate {}",
            fact.predicate
        );
    }
    if !matches!(fact.status.as_str(), "active" | "stale") {
        anyhow::bail!("unsupported markdown memory fact status {}", fact.status);
    }
    if !(0.0..=1.0).contains(&fact.confidence) {
        anyhow::bail!("markdown memory fact confidence out of range");
    }
    if let (Some(valid_from), Some(valid_to)) = (fact.valid_from_epoch, fact.valid_to_epoch) {
        if valid_to < valid_from {
            anyhow::bail!("markdown memory fact valid_to_epoch cannot be before valid_from_epoch");
        }
    }
    Ok(())
}

fn is_supported_fact_predicate(value: &str) -> bool {
    matches!(
        value,
        "fixed_by"
            | "verified_by"
            | "supersedes"
            | "blocked_by"
            | "uses_file"
            | "uses_command"
            | "affects_project"
    )
}