remem-ai 0.6.93

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

use anyhow::Result;
use rusqlite::{params_from_iter, Connection};

use super::sessions::RawSessionSummary;
use crate::memory::session_label::render_from_stored;

pub(super) fn attach_session_labels(
    conn: &Connection,
    sessions: &mut [RawSessionSummary],
) -> Result<()> {
    for session in sessions.iter_mut() {
        apply_label(session, None, None, None);
    }
    if sessions.is_empty() {
        return Ok(());
    }

    let mut by_host_key = HashMap::new();
    load_summary_labels(conn, sessions, &mut by_host_key)?;

    for session in sessions.iter_mut() {
        let host_key = (
            session.host.clone(),
            session.project.clone(),
            session.session_id.clone(),
        );
        let stored = by_host_key.get(&host_key).cloned();
        if let Some((intent, topic, source)) = stored {
            apply_label(
                session,
                intent.as_deref(),
                topic.as_deref(),
                source.as_deref(),
            );
        }
    }
    Ok(())
}

fn apply_label(
    session: &mut RawSessionSummary,
    intent: Option<&str>,
    topic: Option<&str>,
    source: Option<&str>,
) {
    let fallback = session.user_message_samples.first().map(String::as_str);
    let view = render_from_stored(Some(session.first_epoch), intent, topic, source, fallback);
    session.mmdd = view.mmdd;
    session.session_intent = view.session_intent;
    session.session_topic = view.session_topic;
    session.display_label = view.display_label;
    session.session_intent_source = view.session_intent_source;
}

fn load_summary_labels(
    conn: &Connection,
    sessions: &[RawSessionSummary],
    by_host_key: &mut HashMap<
        (String, String, String),
        (Option<String>, Option<String>, Option<String>),
    >,
) -> Result<()> {
    let session_ids = sessions
        .iter()
        .map(|session| session.session_id.as_str())
        .collect::<Vec<_>>();
    if session_ids.is_empty() {
        return Ok(());
    }
    let placeholders = (1..=session_ids.len())
        .map(|index| format!("?{index}"))
        .collect::<Vec<_>>()
        .join(", ");
    let sql = format!(
        "SELECT h.name, COALESCE(p.project_path, p.project_key), s.session_id,
                ss.session_intent, ss.session_topic, ss.session_intent_source
         FROM session_summaries ss
         JOIN sessions s ON s.id = ss.session_row_id
         JOIN hosts h ON h.id = s.host_id
         JOIN projects p ON p.id = s.project_id
         WHERE s.session_id IN ({placeholders})
         ORDER BY COALESCE(ss.session_intent_updated_at_epoch, ss.created_at_epoch) DESC, ss.id DESC"
    );
    let mut statement = conn.prepare(&sql)?;
    let rows = statement.query_map(params_from_iter(session_ids.iter()), |row| {
        Ok((
            row.get::<_, String>(0)?,
            row.get::<_, String>(1)?,
            row.get::<_, String>(2)?,
            row.get::<_, Option<String>>(3)?,
            row.get::<_, Option<String>>(4)?,
            row.get::<_, Option<String>>(5)?,
        ))
    })?;
    for row in rows {
        let (host, project, session_id, intent, topic, source) = row?;
        by_host_key
            .entry((host, project, session_id))
            .or_insert((intent, topic, source));
    }

    Ok(())
}