remem-ai 0.6.93

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

use super::shared::collect_rows;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UserContextStats {
    pub claims_total: i64,
    pub claims_active: i64,
    pub claims_suppressed: i64,
    pub claims_deleted: i64,
    pub candidates_total: i64,
    pub candidates_pending_review: i64,
    pub candidates_auto_promoted: i64,
    pub candidate_block_reasons: Vec<UserContextBlockReasonStat>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserContextBlockReasonStat {
    pub reason: Option<String>,
    pub pending: i64,
}

pub fn query_user_context_stats(conn: &Connection) -> Result<UserContextStats> {
    if !crate::retrieval::temporal::sqlite_table_exists(conn, "user_context_claims")?
        || !crate::retrieval::temporal::sqlite_table_exists(conn, "user_context_candidates")?
    {
        return Ok(UserContextStats::default());
    }

    let candidate_block_reasons = {
        let mut stmt = conn.prepare(
            "SELECT auto_promote_block_reason, COUNT(*) AS pending
             FROM user_context_candidates
             WHERE review_status = 'pending_review'
             GROUP BY auto_promote_block_reason
             ORDER BY pending DESC, auto_promote_block_reason ASC",
        )?;
        let rows = stmt.query_map([], |row| {
            Ok(UserContextBlockReasonStat {
                reason: row.get(0)?,
                pending: row.get(1)?,
            })
        })?;
        collect_rows(rows)?
    };

    Ok(UserContextStats {
        claims_total: count_claims(conn, None)?,
        claims_active: count_claims(conn, Some("active"))?,
        claims_suppressed: count_claims(conn, Some("suppressed"))?,
        claims_deleted: count_claims(conn, Some("deleted"))?,
        candidates_total: count_candidates(conn, None)?,
        candidates_pending_review: count_candidates(conn, Some("pending_review"))?,
        candidates_auto_promoted: count_candidates(conn, Some("auto_promoted"))?,
        candidate_block_reasons,
    })
}

fn count_claims(conn: &Connection, status: Option<&str>) -> Result<i64> {
    match status {
        Some(status) => conn
            .query_row(
                "SELECT COUNT(*) FROM user_context_claims WHERE status = ?1",
                [status],
                |row| row.get(0),
            )
            .map_err(Into::into),
        None => conn
            .query_row("SELECT COUNT(*) FROM user_context_claims", [], |row| {
                row.get(0)
            })
            .map_err(Into::into),
    }
}

fn count_candidates(conn: &Connection, review_status: Option<&str>) -> Result<i64> {
    match review_status {
        Some(review_status) => conn
            .query_row(
                "SELECT COUNT(*) FROM user_context_candidates WHERE review_status = ?1",
                [review_status],
                |row| row.get(0),
            )
            .map_err(Into::into),
        None => conn
            .query_row("SELECT COUNT(*) FROM user_context_candidates", [], |row| {
                row.get(0)
            })
            .map_err(Into::into),
    }
}

#[cfg(test)]
mod tests {
    use rusqlite::Connection;

    use super::*;

    #[test]
    fn user_context_stats_report_claims_candidates_and_block_reasons() -> Result<()> {
        let conn = Connection::open_in_memory()?;
        conn.execute_batch(
            "CREATE TABLE user_context_claims (
                id INTEGER PRIMARY KEY,
                status TEXT NOT NULL
            );
            CREATE TABLE user_context_candidates (
                id INTEGER PRIMARY KEY,
                review_status TEXT NOT NULL,
                auto_promote_block_reason TEXT
            );
            INSERT INTO user_context_claims (status) VALUES
                ('active'),
                ('active'),
                ('suppressed'),
                ('deleted'),
                ('superseded');
            INSERT INTO user_context_candidates (review_status, auto_promote_block_reason) VALUES
                ('pending_review', 'source_not_user_authored'),
                ('pending_review', 'source_not_user_authored'),
                ('pending_review', 'low_confidence'),
                ('auto_promoted', NULL),
                ('approved', NULL);",
        )?;

        let stats = query_user_context_stats(&conn)?;

        assert_eq!(
            stats,
            UserContextStats {
                claims_total: 5,
                claims_active: 2,
                claims_suppressed: 1,
                claims_deleted: 1,
                candidates_total: 5,
                candidates_pending_review: 3,
                candidates_auto_promoted: 1,
                candidate_block_reasons: vec![
                    UserContextBlockReasonStat {
                        reason: Some("source_not_user_authored".to_string()),
                        pending: 2,
                    },
                    UserContextBlockReasonStat {
                        reason: Some("low_confidence".to_string()),
                        pending: 1,
                    },
                ],
            }
        );
        Ok(())
    }

    #[test]
    fn user_context_stats_default_when_tables_are_absent() -> Result<()> {
        let conn = Connection::open_in_memory()?;

        let stats = query_user_context_stats(&conn)?;

        assert_eq!(stats, UserContextStats::default());
        Ok(())
    }
}