remem-ai 0.6.81

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::path::Path;

use anyhow::{Context, Result};
use rusqlite::{params, Connection};

use super::{single_line, LoadedPack, PackImportCategory, PackImportPlan};
use crate::cli::actions::pack_export::{pack_import_routing_reason, PackMemory};
use crate::db::{record_captured_event_with_id_and_reference_time, CaptureEventInput};

const PACK_SOURCE_KIND: &str = "pack";
const PACK_TRUST_CLASS: crate::memory::poisoning::SourceTrustClass =
    crate::memory::poisoning::SourceTrustClass::Pack;

#[derive(Debug, Clone, PartialEq)]
pub(super) struct PackImportApplyReport {
    pub(super) plan: PackImportPlan,
    pub(super) applied: PackImportApplyStats,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(super) struct PackImportApplyStats {
    pub(super) added_memories: usize,
    pub(super) pending_review_candidates: usize,
    pub(super) quarantined_candidates: usize,
}

pub(super) fn apply_loaded_pack(
    conn: &mut Connection,
    target_project: &str,
    loaded: LoadedPack,
) -> Result<PackImportApplyReport> {
    let tx = conn.transaction()?;
    let project_id = ensure_project_row(&tx, target_project)?;
    let plan = super::plan_loaded_pack(Some(&tx), target_project, loaded)?;
    let mut applied = PackImportApplyStats::default();

    for entry in &plan.entries {
        match entry.category {
            PackImportCategory::Add => {
                insert_pack_memory(&tx, target_project, &entry.memory, &plan.content_digest)?;
                applied.added_memories += 1;
            }
            PackImportCategory::Conflict => {
                let candidate_id = insert_pack_candidate(
                    &tx,
                    project_id,
                    target_project,
                    &entry.memory,
                    &plan.content_digest,
                    "pending_review",
                    "pack_import_conflict",
                    None,
                )?;
                if candidate_id != 0 {
                    applied.pending_review_candidates += 1;
                }
            }
            PackImportCategory::Quarantine => {
                let matched = crate::memory::poisoning::scan_instruction_pattern(&format!(
                    "{}\n{}",
                    entry.memory.title, entry.memory.content
                ))
                .context("quarantine plan row lost instruction-pattern match before apply")?;
                let candidate_id = insert_pack_candidate(
                    &tx,
                    project_id,
                    target_project,
                    &entry.memory,
                    &plan.content_digest,
                    "quarantined",
                    "quarantined_instruction_pattern",
                    Some(matched),
                )?;
                if candidate_id != 0 {
                    applied.quarantined_candidates += 1;
                }
            }
            PackImportCategory::Dedup | PackImportCategory::Skip => {}
        }
    }

    tx.commit()?;
    Ok(PackImportApplyReport { plan, applied })
}

pub(super) fn render_import_apply_report(pack: &Path, report: &PackImportApplyReport) -> String {
    let mut output = format!(
        "Pack import applied for {}: added={} pending_review={} quarantined={} (planned add={} dedup={} skip={} conflict={} quarantine={}, digest {}).\n",
        pack.display(),
        report.applied.added_memories,
        report.applied.pending_review_candidates,
        report.applied.quarantined_candidates,
        report.plan.stats.add,
        report.plan.stats.dedup,
        report.plan.stats.skip,
        report.plan.stats.conflict,
        report.plan.stats.quarantine,
        report.plan.content_digest
    );
    for entry in &report.plan.entries {
        output.push_str(&format!(
            "- {} state_key={} hash={} title=\"{}\" reason=\"{}\"\n",
            entry.category.as_str(),
            entry.state_key.as_deref().unwrap_or("-"),
            entry.content_hash,
            single_line(&entry.title),
            single_line(&entry.reason)
        ));
    }
    output
}

fn ensure_project_row(conn: &Connection, target_project: &str) -> Result<i64> {
    let now = chrono::Utc::now().timestamp();
    conn.execute(
        "INSERT INTO workspaces(root_path, git_remote, git_branch, created_at_epoch, updated_at_epoch)
         VALUES (?1, NULL, NULL, ?2, ?2)
         ON CONFLICT(root_path) DO UPDATE SET updated_at_epoch = excluded.updated_at_epoch",
        params![target_project, now],
    )?;
    let workspace_id: i64 = conn.query_row(
        "SELECT id FROM workspaces WHERE root_path = ?1",
        params![target_project],
        |row| row.get(0),
    )?;
    let project_key = target_project
        .rsplit('/')
        .find(|part| !part.is_empty())
        .unwrap_or(target_project);
    conn.execute(
        "INSERT INTO projects(workspace_id, project_path, project_key, created_at_epoch, updated_at_epoch)
         VALUES (?1, ?2, ?3, ?4, ?4)
         ON CONFLICT(workspace_id, project_path) DO UPDATE SET
             project_key = excluded.project_key,
             updated_at_epoch = excluded.updated_at_epoch",
        params![workspace_id, target_project, project_key, now],
    )?;
    conn.query_row(
        "SELECT id FROM projects WHERE workspace_id = ?1 AND project_path = ?2",
        params![workspace_id, target_project],
        |row| row.get(0),
    )
    .map_err(Into::into)
}

fn insert_pack_memory(
    conn: &Connection,
    target_project: &str,
    memory: &PackMemory,
    content_digest: &str,
) -> Result<i64> {
    let now = chrono::Utc::now().timestamp();
    let reference_time = memory.valid_from_epoch.unwrap_or(memory.created_at_epoch);
    let search_context = crate::memory::search_context::build_search_context(
        &memory.memory_type,
        memory.state_key.as_deref(),
        &memory.content,
        None,
    );
    conn.execute(
        "INSERT INTO memories
         (session_id, project, topic_key, title, content, memory_type, files, search_context,
          created_at_epoch, updated_at_epoch, reference_time_epoch, status, branch, scope,
          evidence_event_ids, source_candidate_id, confidence,
          source_project, target_project, owner_scope, owner_key, topic_domain,
          routing_confidence, routing_reason, context_class, expires_at_epoch,
          valid_from_epoch, source_trust_class)
         VALUES (NULL, ?1, ?2, ?3, ?4, ?5, NULL, ?6,
                 ?7, ?8, ?9, 'active', NULL, 'project',
                 '[]', NULL, ?10,
                 ?1, ?1, 'repo', ?1, ?11,
                 1.0, ?12, 'startup_core', ?13,
                 ?14, ?15)",
        params![
            target_project,
            memory.state_key.as_deref(),
            memory.title.as_str(),
            memory.content.as_str(),
            memory.memory_type.as_str(),
            search_context,
            memory.created_at_epoch,
            now,
            reference_time,
            memory.confidence,
            pack_topic_domain(content_digest),
            pack_import_routing_reason(&memory.origin),
            memory.expires_at_epoch,
            memory.valid_from_epoch,
            PACK_TRUST_CLASS.as_str(),
        ],
    )?;
    let memory_id = conn.last_insert_rowid();
    if let Some(state_key) = memory.state_key.as_deref() {
        crate::memory::state_key::attach_current_memory(
            conn,
            memory_id,
            "repo",
            target_project,
            &memory.memory_type,
            &crate::memory::state_key::StateKeyDecision {
                state_key: state_key.to_string(),
                confidence: memory.state_key_confidence.unwrap_or(1.0),
                reason: memory
                    .state_key_reason
                    .clone()
                    .unwrap_or_else(|| "pack_import".to_string()),
            },
            now,
        )?;
    }
    refresh_imported_memory_indexes(conn, memory_id, &memory.title, &memory.content)?;
    Ok(memory_id)
}

fn insert_pack_candidate(
    conn: &Connection,
    project_id: i64,
    target_project: &str,
    memory: &PackMemory,
    content_digest: &str,
    review_status: &str,
    block_reason: &str,
    quarantine_match: Option<crate::memory::poisoning::InstructionPatternMatch>,
) -> Result<i64> {
    let now = chrono::Utc::now().timestamp();
    let topic_key = memory
        .state_key
        .as_deref()
        .unwrap_or(memory.content_hash.as_str());
    if existing_pack_candidate(conn, target_project, memory, review_status, block_reason)? {
        return Ok(0);
    }
    let evidence_event_id =
        record_pack_candidate_evidence(conn, target_project, memory, content_digest, block_reason)?;
    let evidence_json = serde_json::to_string(&vec![evidence_event_id])?;
    let candidate_text = encode_pack_review_text(&memory.title, &memory.content);
    conn.execute(
        "INSERT INTO memory_candidates
         (project_id, scope, memory_type, topic_key, text, evidence_event_ids,
          confidence, risk_class, review_status, created_at_epoch, updated_at_epoch,
          auto_promote_block_reason, source_project, target_project, owner_scope, owner_key,
          topic_domain, routing_confidence, routing_reason, context_class, expires_at_epoch,
         valid_from_epoch, state_key, state_key_confidence, state_key_reason,
         source_kind, source_trust_class, quarantine_pattern_id, quarantine_pattern_version)
         VALUES (?1, 'project', ?2, ?3, ?4, ?5,
                 ?6, ?7, ?8, ?9, ?9,
                 ?10, ?11, ?11, 'repo', ?11,
                 ?12, 1.0, ?13, 'startup_core', ?14,
                 ?15, ?16, ?17, ?18,
                 ?19, ?20, ?21, ?22)",
        params![
            project_id,
            memory.memory_type.as_str(),
            topic_key,
            candidate_text,
            evidence_json,
            memory.confidence.unwrap_or(0.5),
            if review_status == "quarantined" {
                "high"
            } else {
                "medium"
            },
            review_status,
            now,
            block_reason,
            target_project,
            pack_topic_domain(content_digest),
            pack_import_routing_reason(&memory.origin),
            memory.expires_at_epoch,
            memory.valid_from_epoch,
            memory.state_key.as_deref(),
            memory.state_key_confidence,
            memory.state_key_reason.as_deref(),
            PACK_SOURCE_KIND,
            PACK_TRUST_CLASS.as_str(),
            quarantine_match.map(|matched| matched.pattern_id),
            quarantine_match.map(|matched| matched.pattern_set_version),
        ],
    )?;
    Ok(conn.last_insert_rowid())
}

fn existing_pack_candidate(
    conn: &Connection,
    target_project: &str,
    memory: &PackMemory,
    review_status: &str,
    block_reason: &str,
) -> Result<bool> {
    let topic_key = memory
        .state_key
        .as_deref()
        .unwrap_or(memory.content_hash.as_str());
    let text = encode_pack_review_text(&memory.title, &memory.content);
    let count: i64 = conn.query_row(
        "SELECT COUNT(*)
         FROM memory_candidates
         WHERE source_kind = ?1
           AND source_trust_class = ?2
           AND target_project = ?3
           AND memory_type = ?4
           AND topic_key = ?5
           AND text = ?6
           AND review_status = ?7
           AND auto_promote_block_reason = ?8",
        params![
            PACK_SOURCE_KIND,
            PACK_TRUST_CLASS.as_str(),
            target_project,
            memory.memory_type.as_str(),
            topic_key,
            text,
            review_status,
            block_reason,
        ],
        |row| row.get(0),
    )?;
    Ok(count > 0)
}

fn encode_pack_review_text(title: &str, content: &str) -> String {
    format!("pack_title: {}\npack_content:\n{}", title.trim(), content)
}

fn record_pack_candidate_evidence(
    conn: &Connection,
    target_project: &str,
    memory: &PackMemory,
    content_digest: &str,
    block_reason: &str,
) -> Result<i64> {
    let digest_prefix = content_digest.chars().take(12).collect::<String>();
    let content = format!(
        "pack_import_digest={digest_prefix}\nblock_reason={block_reason}\ntitle={}\ncontent={}",
        memory.title, memory.content
    );
    let event_id = format!("pack-import:{digest_prefix}:{}", memory.content_hash);
    let reference_time = memory.valid_from_epoch.or(Some(memory.created_at_epoch));
    let outcome = record_captured_event_with_id_and_reference_time(
        conn,
        &CaptureEventInput {
            host: "codex-cli",
            session_id: &format!("pack-import:{digest_prefix}"),
            project: target_project,
            cwd: None,
            event_type: "pack_import",
            role: None,
            tool_name: Some("remem import"),
            content: &content,
            task_kind: None,
        },
        Some(&event_id),
        reference_time,
    )?;
    Ok(outcome.event_row_id)
}

fn pack_topic_domain(digest: &str) -> String {
    let prefix = digest.chars().take(12).collect::<String>();
    format!("pack:{prefix}")
}

fn refresh_imported_memory_indexes(
    conn: &Connection,
    memory_id: i64,
    title: &str,
    content: &str,
) -> Result<()> {
    let entities = crate::retrieval::entity::extract_entities(title, content);
    crate::retrieval::entity::refresh_memory_entities(conn, memory_id, &entities)?;
    crate::retrieval::vector::upsert_memory_embedding_for_row(conn, memory_id)
}