remem-ai 0.5.150

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use axum::{
    extract::{Query, State},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use rusqlite::types::ToSql;

use super::super::helpers::{error_response, open_request_db};
use super::super::types::{CandidateItem, CandidateParams, DbState, ListMeta, ListResponse};

pub(in crate::api) async fn handle_list_candidates(
    State(_state): State<DbState>,
    Query(params): Query<CandidateParams>,
) -> impl IntoResponse {
    let conn = match open_request_db() {
        Ok(conn) => conn,
        Err(response) => return response,
    };

    let status = params
        .status
        .as_deref()
        .filter(|s| !s.is_empty())
        .unwrap_or("pending_review");
    let limit = params.limit.unwrap_or(50).clamp(1, 100);
    let offset = params.offset.unwrap_or(0).max(0);

    let mut conditions = vec!["c.review_status = ?1".to_string()];
    let mut binds: Vec<Box<dyn ToSql>> = vec![Box::new(status.to_string())];
    let mut idx = 2usize;
    if let Some(project) = params.project.as_deref().filter(|s| !s.is_empty()) {
        push_candidate_project_filter(project, &mut idx, &mut conditions, &mut binds);
    }
    let where_sql = conditions.join(" AND ");

    let count_sql = format!(
        "SELECT COUNT(*) FROM memory_candidates c \
         LEFT JOIN projects p ON p.id = c.project_id \
         WHERE {where_sql}"
    );
    let binds_refs = crate::db::to_sql_refs(&binds);
    let total: i64 = match conn.query_row(&count_sql, binds_refs.as_slice(), |row| row.get(0)) {
        Ok(n) => n,
        Err(err) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "count_failed",
                &err.to_string(),
            )
            .into_response()
        }
    };

    let result = (|| -> anyhow::Result<Vec<CandidateItem>> {
        let sql = format!(
            "SELECT c.id, c.memory_type, c.text, c.scope, c.confidence, c.risk_class, \
                    c.review_status, c.evidence_event_ids, c.created_at_epoch, \
                    COALESCE(c.target_project, p.project_path, c.source_project) AS project \
             FROM memory_candidates c \
             LEFT JOIN projects p ON p.id = c.project_id \
             WHERE {where_sql} ORDER BY c.created_at_epoch DESC LIMIT ?{idx} OFFSET ?{}",
            idx + 1,
        );
        binds.push(Box::new(limit));
        binds.push(Box::new(offset));
        let mut stmt = conn.prepare(&sql)?;
        let binds_refs2 = crate::db::to_sql_refs(&binds);
        let rows = stmt.query_map(binds_refs2.as_slice(), |row| {
            let evidence_json: Option<String> = row.get(7)?;
            let evidence_count = evidence_json
                .as_deref()
                .and_then(|s| serde_json::from_str::<Vec<serde_json::Value>>(s).ok())
                .map(|v| v.len() as i64)
                .unwrap_or(0);
            Ok(CandidateItem {
                id: row.get(0)?,
                project: row.get(9)?,
                memory_type: row.get(1)?,
                text: row.get(2)?,
                scope: row.get(3)?,
                confidence: row.get(4)?,
                risk_class: row.get(5)?,
                review_status: row.get(6)?,
                evidence_count,
                created_at_epoch: row.get(8)?,
            })
        })?;
        rows.collect::<Result<Vec<_>, rusqlite::Error>>()
            .map_err(anyhow::Error::from)
    })();

    let items = match result {
        Ok(v) => v,
        Err(err) => {
            return error_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "list_failed",
                &err.to_string(),
            )
            .into_response()
        }
    };

    let count = items.len();
    Json(ListResponse {
        data: items,
        meta: ListMeta {
            count,
            total,
            limit,
            offset,
        },
    })
    .into_response()
}

fn push_candidate_project_filter(
    project: &str,
    idx: &mut usize,
    conditions: &mut Vec<String>,
    binds: &mut Vec<Box<dyn ToSql>>,
) {
    let project_idx = *idx;
    binds.push(Box::new(project.to_string()));
    *idx += 1;
    let source_idx = *idx;
    binds.push(Box::new(project.to_string()));
    *idx += 1;
    let target_idx = *idx;
    binds.push(Box::new(project.to_string()));
    *idx += 1;
    let owner_idx = *idx;
    binds.push(Box::new(project.to_string()));
    *idx += 1;
    conditions.push(format!(
        "(p.project_path = ?{project_idx} \
          OR c.source_project = ?{source_idx} \
          OR c.target_project = ?{target_idx} \
          OR (c.owner_scope = 'repo' AND c.owner_key = ?{owner_idx}))"
    ));
}