remem-ai 0.4.9

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

use crate::memory::lesson::{is_lesson_candidate, save_lesson, SaveLessonRequest};
use crate::memory::{insert_memory, insert_memory_full};

use super::format::{
    build_content, build_item_title, build_title, split_into_items, MIN_DECISION_LEN,
};
use super::slug::content_hash;

const MIN_LEARNED_LEN: usize = 30;
const MIN_PREFERENCE_LEN: usize = 10;

pub fn promote_summary_to_memories(
    conn: &Connection,
    session_id: &str,
    project: &str,
    request: Option<&str>,
    decisions: Option<&str>,
    learned: Option<&str>,
    preferences: Option<&str>,
) -> Result<usize> {
    let request_text = request.unwrap_or("").trim();
    let mut count = 0;

    if let Some(text) = decisions {
        count += promote_standard_items(
            conn,
            session_id,
            project,
            request_text,
            text,
            MIN_DECISION_LEN,
            "decision",
            "decisions",
            "decision",
            "decision",
        )?;
    }

    if let Some(text) = learned {
        count += promote_learned_items(conn, session_id, project, request_text, text)?;
    }

    if let Some(text) = preferences {
        let text = text.trim();
        if text.len() >= MIN_PREFERENCE_LEN {
            let title = build_title(text, "", "preference");
            let topic_key = format!("preference-{}", content_hash(text));
            insert_memory_full(
                conn,
                Some(session_id),
                project,
                Some(&topic_key),
                &title,
                text,
                "preference",
                None,
                None,
                "project",
                None,
            )?;
            count += 1;
        }
    }

    if count > 0 {
        crate::log::info(
            "promote",
            &format!(
                "promoted {} memories from summary project={}",
                count, project
            ),
        );
    }

    Ok(count)
}

fn promote_learned_items(
    conn: &Connection,
    session_id: &str,
    project: &str,
    request_text: &str,
    text: &str,
) -> Result<usize> {
    let text = text.trim();
    if text.len() < MIN_LEARNED_LEN {
        return Ok(0);
    }

    let split_items = split_into_items(text);
    let items: Vec<&str> = if split_items.len() > 1 {
        split_items
            .iter()
            .map(String::as_str)
            .filter(|item| item.len() >= MIN_LEARNED_LEN)
            .collect()
    } else {
        vec![text]
    };

    let mut count = 0;
    for (index, item) in items.iter().enumerate() {
        let content = build_content(item, request_text);
        if is_lesson_candidate(item) {
            let title = if items.len() > 1 {
                build_item_title(item, "lesson", index)
            } else {
                build_title(item, request_text, "lesson")
            };
            let topic_key = format!("lesson-{}", content_hash(item));
            save_lesson(
                conn,
                &SaveLessonRequest {
                    session_id: Some(session_id),
                    project,
                    topic_key: Some(&topic_key),
                    title: &title,
                    content: &content,
                    confidence: lesson_confidence(item),
                    source_evidence: (!request_text.is_empty()).then_some(request_text),
                    files: None,
                    branch: None,
                    scope: "project",
                    created_at_epoch: None,
                    stale_after_epoch: None,
                },
            )?;
        } else {
            let title = if items.len() > 1 {
                build_item_title(item, "learned", index)
            } else {
                build_title(item, request_text, "learned")
            };
            let topic_key = format!("discovery-{}", content_hash(item));
            insert_memory(
                conn,
                Some(session_id),
                project,
                Some(&topic_key),
                &title,
                &content,
                "discovery",
                None,
            )?;
        }
        count += 1;
    }
    Ok(count)
}

fn lesson_confidence(item: &str) -> f64 {
    let normalized = item.to_lowercase();
    if normalized.contains("root cause") || normalized.contains("lesson:") {
        0.85
    } else if normalized.contains("never ") || normalized.contains("do not ") {
        0.8
    } else {
        0.7
    }
}

fn promote_standard_items(
    conn: &Connection,
    session_id: &str,
    project: &str,
    request_text: &str,
    text: &str,
    min_len: usize,
    item_label: &str,
    single_label: &str,
    topic_prefix: &str,
    memory_type: &str,
) -> Result<usize> {
    let text = text.trim();
    if text.len() < min_len {
        return Ok(0);
    }

    let items = split_into_items(text);
    if items.len() > 1 {
        let mut count = 0;
        for (index, item) in items.iter().enumerate() {
            if item.len() < min_len {
                continue;
            }
            let title = build_item_title(item, item_label, index);
            let content = build_content(item, request_text);
            let topic_key = format!("{}-{}", topic_prefix, content_hash(item));
            insert_memory(
                conn,
                Some(session_id),
                project,
                Some(&topic_key),
                &title,
                &content,
                memory_type,
                None,
            )?;
            count += 1;
        }
        Ok(count)
    } else {
        let title = build_title(text, request_text, single_label);
        let content = build_content(text, request_text);
        let topic_key = format!("{}-{}", topic_prefix, content_hash(text));
        insert_memory(
            conn,
            Some(session_id),
            project,
            Some(&topic_key),
            &title,
            &content,
            memory_type,
            None,
        )?;
        Ok(1)
    }
}