remem-ai 0.5.207

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

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryGovernanceAction {
    Delete,
    Reject,
    MarkStale,
    AcknowledgePattern,
}

impl MemoryGovernanceAction {
    pub fn parse(value: &str) -> Result<Self> {
        match value.trim().to_lowercase().as_str() {
            "delete" | "deleted" => Ok(Self::Delete),
            "reject" | "rejected" => Ok(Self::Reject),
            "stale" | "mark_stale" | "mark-stale" | "invalidate" => Ok(Self::MarkStale),
            "acknowledge_pattern" | "acknowledge-pattern" | "ack" => Ok(Self::AcknowledgePattern),
            other => bail!("unsupported memory governance action: {other}"),
        }
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Self::Delete => "delete",
            Self::Reject => "reject",
            Self::MarkStale => "stale",
            Self::AcknowledgePattern => "acknowledge_pattern",
        }
    }

    pub fn target_status(self) -> &'static str {
        match self {
            Self::Delete => "deleted",
            Self::Reject => "rejected",
            Self::MarkStale => "stale",
            Self::AcknowledgePattern => "active",
        }
    }
}

#[derive(Debug, Clone)]
pub struct GovernMemoryRequest<'a> {
    pub project: &'a str,
    pub ids: &'a [i64],
    pub action: MemoryGovernanceAction,
    pub reason: Option<&'a str>,
    pub actor: Option<&'a str>,
    pub dry_run: bool,
    pub confirm_destructive: bool,
    pub acknowledge_pattern: Option<&'a str>,
}

#[derive(Debug, Clone, Serialize)]
pub struct GovernedMemory {
    pub id: i64,
    pub title: String,
    pub previous_status: String,
    pub new_status: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct GovernMemoryResult {
    pub dry_run: bool,
    pub action: String,
    pub reason: Option<String>,
    pub affected: Vec<GovernedMemory>,
}

#[derive(Debug, Clone)]
pub struct GovernanceSelector<'a> {
    pub project: &'a str,
    pub query: Option<&'a str>,
    pub memory_type: Option<&'a str>,
    pub status: Option<&'a str>,
    pub limit: i64,
    pub offset: i64,
}

pub fn select_memory_ids(conn: &Connection, selector: &GovernanceSelector<'_>) -> Result<Vec<i64>> {
    let mut conditions = vec!["project = ?1".to_string()];
    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> =
        vec![Box::new(selector.project.to_string())];
    let mut idx = 2;

    if let Some(status) = normalized_status_filter(selector.status)? {
        conditions.push(format!("status = ?{idx}"));
        params.push(Box::new(status));
        idx += 1;
    }

    if let Some(memory_type) = trimmed(selector.memory_type) {
        conditions.push(format!("memory_type = ?{idx}"));
        params.push(Box::new(memory_type.to_string()));
        idx += 1;
    }

    if let Some(query) = trimmed(selector.query) {
        let pattern = like_pattern(query);
        conditions.push(format!(
            "(title LIKE ?{idx} ESCAPE '\\' \
             OR content LIKE ?{next_idx} ESCAPE '\\' \
             OR COALESCE(search_context, '') LIKE ?{third_idx} ESCAPE '\\')",
            idx = idx,
            next_idx = idx + 1,
            third_idx = idx + 2
        ));
        params.push(Box::new(pattern.clone()));
        params.push(Box::new(pattern.clone()));
        params.push(Box::new(pattern));
        idx += 3;
    }

    params.push(Box::new(selector.limit.max(1)));
    params.push(Box::new(selector.offset.max(0)));
    let sql = format!(
        "SELECT id FROM memories \
         WHERE {} \
         ORDER BY updated_at_epoch DESC, id DESC \
         LIMIT ?{} OFFSET ?{}",
        conditions.join(" AND "),
        idx,
        idx + 1
    );
    let mut stmt = conn.prepare(&sql)?;
    let refs = crate::db::to_sql_refs(&params);
    let rows = stmt.query_map(refs.as_slice(), |row| row.get::<_, i64>(0))?;
    crate::db::query::collect_rows(rows)
}

pub fn govern_memories(
    conn: &Connection,
    req: &GovernMemoryRequest<'_>,
) -> Result<GovernMemoryResult> {
    let ids = unique_ids(req.ids);
    if ids.is_empty() {
        bail!("memory governance requires at least one memory id");
    }
    let reason = normalized_reason(req)?;
    let acknowledged_pattern = normalized_acknowledge_pattern(req)?;
    let target_status = req.action.target_status();
    let tx = conn.unchecked_transaction()?;
    let mut affected = Vec::with_capacity(ids.len());
    let mut rule_source_ids = Vec::new();
    for id in ids {
        let target = load_target(&tx, req.project, id)?;
        if req.action == MemoryGovernanceAction::AcknowledgePattern {
            validate_acknowledgement(&target, acknowledged_pattern)?;
        }
        let new_status = if req.action == MemoryGovernanceAction::AcknowledgePattern {
            target.status.as_str()
        } else {
            target_status
        };
        affected.push(GovernedMemory {
            id: target.id,
            title: target.title.clone(),
            previous_status: target.status.clone(),
            new_status: new_status.to_string(),
        });
        if req.dry_run {
            continue;
        }
        if req.action != MemoryGovernanceAction::AcknowledgePattern {
            rule_source_ids.push(target.id);
        }
        let now = chrono::Utc::now().timestamp();
        let updated = if req.action == MemoryGovernanceAction::AcknowledgePattern {
            tx.execute(
                "UPDATE memories
                 SET acknowledged_pattern_id = ?1,
                     acknowledged_pattern_version = ?2,
                     acknowledged_at_epoch = ?3,
                     updated_at_epoch = ?3
                 WHERE id = ?4 AND project = ?5",
                params![
                    acknowledged_pattern,
                    crate::memory::poisoning::INSTRUCTION_PATTERN_SET_VERSION,
                    now,
                    target.id,
                    req.project
                ],
            )?
        } else {
            tx.execute(
                "UPDATE memories
                 SET status = ?1, updated_at_epoch = ?2
                 WHERE id = ?3 AND project = ?4",
                params![target_status, now, target.id, req.project],
            )?
        };
        if updated != 1 {
            return Err(anyhow!(
                "failed to update memory governance target: id={} project={}",
                target.id,
                req.project
            ));
        }
        insert_audit_event(&tx, req, &target, new_status, reason.as_deref(), now)?;
    }
    crate::memory::preference::compilation::enqueue_for_memory_ids(&tx, &rule_source_ids)?;
    tx.commit()?;
    Ok(GovernMemoryResult {
        dry_run: req.dry_run,
        action: req.action.as_str().to_string(),
        reason,
        affected,
    })
}

fn unique_ids(ids: &[i64]) -> Vec<i64> {
    let mut seen = std::collections::HashSet::with_capacity(ids.len());
    ids.iter()
        .copied()
        .filter(|id| *id > 0 && seen.insert(*id))
        .collect()
}

fn trimmed(value: Option<&str>) -> Option<&str> {
    value.map(str::trim).filter(|value| !value.is_empty())
}

fn normalized_status_filter(status: Option<&str>) -> Result<Option<String>> {
    let Some(status) = trimmed(status) else {
        return Ok(Some("active".to_string()));
    };
    let normalized = status.to_lowercase();
    if matches!(normalized.as_str(), "all" | "*") {
        return Ok(None);
    }
    if matches!(
        normalized.as_str(),
        "active" | "stale" | "rejected" | "deleted" | "archived" | "superseded"
    ) {
        return Ok(Some(normalized));
    }
    bail!("unsupported memory status filter: {status}");
}

fn like_pattern(query: &str) -> String {
    let mut pattern = String::with_capacity(query.len() + 2);
    pattern.push('%');
    for ch in query.chars() {
        if matches!(ch, '%' | '_' | '\\') {
            pattern.push('\\');
        }
        pattern.push(ch);
    }
    pattern.push('%');
    pattern
}

fn normalized_reason(req: &GovernMemoryRequest<'_>) -> Result<Option<String>> {
    let reason = req.reason.map(str::trim).filter(|value| !value.is_empty());
    if req.dry_run {
        return Ok(reason.map(str::to_string));
    }
    if !req.confirm_destructive {
        bail!("memory governance mutation requires confirm_destructive=true");
    }
    let Some(reason) = reason else {
        bail!("memory governance mutation requires an explicit reason");
    };
    Ok(Some(reason.to_string()))
}

fn normalized_acknowledge_pattern<'a>(req: &'a GovernMemoryRequest<'a>) -> Result<Option<&'a str>> {
    let pattern = req
        .acknowledge_pattern
        .map(str::trim)
        .filter(|value| !value.is_empty());
    if req.action == MemoryGovernanceAction::AcknowledgePattern && pattern.is_none() {
        bail!("acknowledge_pattern action requires acknowledge_pattern");
    }
    if req.action != MemoryGovernanceAction::AcknowledgePattern && pattern.is_some() {
        bail!("acknowledge_pattern is only valid with acknowledge_pattern action");
    }
    Ok(pattern)
}

fn validate_acknowledgement(
    target: &GovernanceTarget,
    acknowledged_pattern: Option<&str>,
) -> Result<()> {
    let acknowledged_pattern = acknowledged_pattern.expect("validated acknowledge_pattern");
    let Some(matched) = crate::memory::poisoning::scan_instruction_pattern(&format!(
        "{}\n{}",
        target.title, target.content
    )) else {
        bail!(
            "memory id={} does not match an instruction-pattern; cannot acknowledge {}",
            target.id,
            acknowledged_pattern
        );
    };
    if matched.pattern_id != acknowledged_pattern {
        bail!(
            "memory id={} acknowledged pattern {} does not match instruction-pattern {}@v{}",
            target.id,
            acknowledged_pattern,
            matched.pattern_id,
            matched.pattern_set_version
        );
    }
    Ok(())
}

struct GovernanceTarget {
    id: i64,
    title: String,
    content: String,
    status: String,
}

fn load_target(conn: &Connection, project: &str, id: i64) -> Result<GovernanceTarget> {
    conn.query_row(
        "SELECT id, title, content, status
         FROM memories
         WHERE id = ?1 AND project = ?2",
        params![id, project],
        |row| {
            Ok(GovernanceTarget {
                id: row.get(0)?,
                title: row.get(1)?,
                content: row.get(2)?,
                status: row.get(3)?,
            })
        },
    )
    .optional()?
    .ok_or_else(|| anyhow!("memory id={} not found in project={}", id, project))
}

fn insert_audit_event(
    conn: &Connection,
    req: &GovernMemoryRequest<'_>,
    target: &GovernanceTarget,
    new_status: &str,
    reason: Option<&str>,
    now: i64,
) -> Result<()> {
    let actor = req.actor.map(str::trim).filter(|value| !value.is_empty());
    let detail = serde_json::json!({
        "action": req.action.as_str(),
        "memory_id": target.id,
        "title": target.title,
        "previous_status": target.status,
        "new_status": new_status,
        "reason": reason,
        "actor": actor,
        "acknowledged_pattern": req.acknowledge_pattern,
    })
    .to_string();
    let summary = format!(
        "{} memory {}: {} -> {}{}",
        req.action.as_str(),
        target.id,
        target.status,
        new_status,
        reason
            .map(|value| format!(" ({value})"))
            .unwrap_or_default()
    );
    conn.execute(
        "INSERT INTO events
         (session_id, project, event_type, summary, detail, files, exit_code, created_at_epoch)
         VALUES (?1, ?2, 'memory_governance', ?3, ?4, NULL, NULL, ?5)",
        params![
            actor.unwrap_or("memory-governance"),
            req.project,
            summary,
            detail,
            now
        ],
    )?;
    Ok(())
}

#[cfg(test)]
mod tests;