remem-ai 0.6.71

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

use super::path::{file_path_overlaps, parse_json_file_array};

pub(super) const SOURCE_COMMIT_FILES_SQL: &str = "
    SELECT c.id,
           COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch),
           c.branch,
           f.path
    FROM git_commit_sessions AS l
    CROSS JOIN git_commits AS c ON c.id = l.commit_id
    CROSS JOIN git_commit_files AS f ON f.commit_id = c.id
    WHERE c.project = ?1
      AND (l.memory_session_id = ?2 OR l.session_id = ?2)
      AND (?3 IS NULL OR c.branch = ?3 OR c.branch IS NULL)
      AND COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch) <= ?4
    ORDER BY COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch) DESC,
             c.id DESC";

pub(super) const SOURCE_COMMIT_JSON_SQL: &str = "
    SELECT c.id,
           COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch),
           c.branch,
           c.changed_files
    FROM git_commit_sessions AS l
    CROSS JOIN git_commits AS c ON c.id = l.commit_id
    WHERE c.project = ?1
      AND (l.memory_session_id = ?2 OR l.session_id = ?2)
      AND (?3 IS NULL OR c.branch = ?3 OR c.branch IS NULL)
      AND COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch) <= ?4
    ORDER BY COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch) DESC,
             c.id DESC";

pub(super) const LATER_COMMIT_FILES_SQL: &str = "
    SELECT f.path
    FROM git_commits AS c
    CROSS JOIN git_commit_files AS f ON f.commit_id = c.id
    WHERE c.project = ?1
      AND COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch) > ?2
      AND (?4 IS NULL OR c.branch = ?4 OR c.branch IS NULL)
    UNION ALL
    SELECT f.path
    FROM git_commits AS c
    CROSS JOIN git_commit_files AS f ON f.commit_id = c.id
    WHERE c.project = ?1
      AND COALESCE(c.authored_at_epoch, c.updated_at_epoch, c.created_at_epoch) = ?2
      AND c.id > ?3
      AND (?4 IS NULL OR c.branch = ?4 OR c.branch IS NULL)";

const LATER_COMMIT_JSON_SQL: &str = "
    SELECT changed_files
    FROM git_commits
    WHERE project = ?1
      AND COALESCE(authored_at_epoch, updated_at_epoch, created_at_epoch) > ?2
      AND (?4 IS NULL OR branch = ?4 OR branch IS NULL)
    UNION ALL
    SELECT changed_files
    FROM git_commits
    WHERE project = ?1
      AND COALESCE(authored_at_epoch, updated_at_epoch, created_at_epoch) = ?2
      AND id > ?3
      AND (?4 IS NULL OR branch = ?4 OR branch IS NULL)";

#[derive(Debug, Clone)]
pub(super) struct SourceAnchor {
    pub(super) id: i64,
    pub(super) epoch: i64,
    pub(super) branch: Option<String>,
}

pub(super) fn source_commit_anchor_for_file_sessions(
    conn: &Connection,
    project: &str,
    session_ids: &[String],
    branch_filter: Option<&str>,
    max_epoch: i64,
    touched_file: &str,
    use_commit_files: bool,
) -> Result<Option<SourceAnchor>> {
    let mut latest = None;
    for session_id in session_ids {
        let Some(anchor) = source_commit_anchor_for_session(
            conn,
            project,
            session_id,
            branch_filter,
            max_epoch,
            touched_file,
            use_commit_files,
        )?
        else {
            continue;
        };
        if latest.as_ref().is_none_or(|current: &SourceAnchor| {
            (anchor.epoch, anchor.id) > (current.epoch, current.id)
        }) {
            latest = Some(anchor);
        }
    }
    Ok(latest)
}

fn source_commit_anchor_for_session(
    conn: &Connection,
    project: &str,
    session_id: &str,
    branch_filter: Option<&str>,
    max_epoch: i64,
    touched_file: &str,
    use_commit_files: bool,
) -> Result<Option<SourceAnchor>> {
    if use_commit_files {
        source_commit_anchor_from_files(
            conn,
            project,
            session_id,
            branch_filter,
            max_epoch,
            touched_file,
        )
    } else {
        source_commit_anchor_from_json(
            conn,
            project,
            session_id,
            branch_filter,
            max_epoch,
            touched_file,
        )
    }
}

fn source_commit_anchor_from_files(
    conn: &Connection,
    project: &str,
    session_id: &str,
    branch_filter: Option<&str>,
    max_epoch: i64,
    touched_file: &str,
) -> Result<Option<SourceAnchor>> {
    let mut statement = conn.prepare_cached(SOURCE_COMMIT_FILES_SQL)?;
    let rows = statement.query_map(
        params![project, session_id, branch_filter, max_epoch],
        |row| {
            Ok((
                SourceAnchor {
                    id: row.get(0)?,
                    epoch: row.get(1)?,
                    branch: row.get(2)?,
                },
                row.get::<_, String>(3)?,
            ))
        },
    )?;
    for row in rows {
        let (anchor, changed_file) = row?;
        if file_path_overlaps(&changed_file, touched_file, project) {
            return Ok(Some(anchor));
        }
    }
    Ok(None)
}

fn source_commit_anchor_from_json(
    conn: &Connection,
    project: &str,
    session_id: &str,
    branch_filter: Option<&str>,
    max_epoch: i64,
    touched_file: &str,
) -> Result<Option<SourceAnchor>> {
    let mut statement = conn.prepare_cached(SOURCE_COMMIT_JSON_SQL)?;
    let rows = statement.query_map(
        params![project, session_id, branch_filter, max_epoch],
        |row| {
            Ok((
                SourceAnchor {
                    id: row.get(0)?,
                    epoch: row.get(1)?,
                    branch: row.get(2)?,
                },
                row.get::<_, String>(3)?,
            ))
        },
    )?;
    for row in rows {
        let (anchor, raw_changed_files) = row?;
        let changed_files = parse_json_file_array(&raw_changed_files).with_context(|| {
            format!(
                "parse git commit {} changed_files for source-anchor staleness",
                anchor.id
            )
        })?;
        if changed_files
            .iter()
            .any(|changed_file| file_path_overlaps(changed_file, touched_file, project))
        {
            return Ok(Some(anchor));
        }
    }
    Ok(None)
}

pub(super) fn later_commit_touches_file(
    conn: &Connection,
    project: &str,
    anchor: &SourceAnchor,
    branch_filter: Option<&str>,
    touched_file: &str,
    use_commit_files: bool,
) -> Result<bool> {
    if use_commit_files {
        later_commit_file_relation_touches(conn, project, anchor, branch_filter, touched_file)
    } else {
        later_commit_json_touches(conn, project, anchor, branch_filter, touched_file)
    }
}

fn later_commit_file_relation_touches(
    conn: &Connection,
    project: &str,
    anchor: &SourceAnchor,
    branch_filter: Option<&str>,
    touched_file: &str,
) -> Result<bool> {
    let mut statement = conn.prepare_cached(LATER_COMMIT_FILES_SQL)?;
    let mut rows = statement.query(params![project, anchor.epoch, anchor.id, branch_filter])?;
    while let Some(row) = rows.next()? {
        let changed_file: String = row.get(0)?;
        if file_path_overlaps(&changed_file, touched_file, project) {
            return Ok(true);
        }
    }
    Ok(false)
}

fn later_commit_json_touches(
    conn: &Connection,
    project: &str,
    anchor: &SourceAnchor,
    branch_filter: Option<&str>,
    touched_file: &str,
) -> Result<bool> {
    let mut statement = conn.prepare_cached(LATER_COMMIT_JSON_SQL)?;
    let mut rows = statement.query(params![project, anchor.epoch, anchor.id, branch_filter])?;
    while let Some(row) = rows.next()? {
        let raw: String = row.get(0)?;
        let changed_files = parse_json_file_array(&raw)
            .with_context(|| "parse git commit changed_files for source-anchor staleness")?;
        if changed_files
            .iter()
            .any(|changed_file| file_path_overlaps(changed_file, touched_file, project))
        {
            return Ok(true);
        }
    }
    Ok(false)
}

#[cfg(test)]
mod tests;