remem-ai 0.6.70

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

use anyhow::{ensure, Context, Result};
use rusqlite::Connection;

use crate::memory::{self, Memory};

use super::support::{candidate_confidence, contributions_for, visibility_label};
use super::QuerySearchPlan;
use crate::retrieval::search::{
    ChannelHit, SearchExplain, SearchExplainChannel, SearchExplainDetails, SearchExplainResult,
    SearchExplainResultBreakdown,
};

#[allow(clippy::too_many_arguments)]
pub(super) fn build_explain(
    conn: &Connection,
    query_text: &str,
    project: Option<&str>,
    memory_type: Option<&str>,
    limit: i64,
    offset: i64,
    include_stale: bool,
    branch: Option<&str>,
    plan: &QuerySearchPlan,
    fusion_scores: &[(i64, f64)],
    final_scores: &[(i64, f64)],
    filtered_result_count: usize,
    paged: &[Memory],
) -> Result<SearchExplainDetails> {
    let channels = plan
        .channels
        .iter()
        .map(|channel| SearchExplainChannel {
            name: channel.name.to_string(),
            enabled: channel.is_enabled(),
            disabled_reason: channel.disabled_reason.clone(),
            candidates_scanned: channel.candidates_scanned,
            embedding: channel.embedding.clone(),
            hits: channel
                .hits
                .iter()
                .enumerate()
                .map(|(index, hit)| ChannelHit {
                    memory_id: hit.id,
                    rank: index + 1,
                })
                .collect(),
        })
        .collect();
    let id_to_fusion_score: HashMap<i64, f64> = fusion_scores.iter().copied().collect();
    let id_to_final_score: HashMap<i64, f64> = final_scores.iter().copied().collect();
    let now_epoch = chrono::Utc::now().timestamp();
    let staleness_labels =
        memory::staleness::memory_staleness_labels_for_memories(conn, paged, now_epoch)?;
    let explained_results = paged
        .iter()
        .enumerate()
        .map(|(index, memory)| {
            explain_result(
                memory,
                index,
                project,
                plan,
                &id_to_fusion_score,
                &id_to_final_score,
                &staleness_labels,
                now_epoch,
            )
        })
        .collect::<Result<Vec<_>>>()?;
    let (results, contribution_breakdowns) = explained_results.into_iter().unzip();
    Ok(SearchExplainDetails {
        explain: SearchExplain {
            query: query_text.to_string(),
            project: project.map(str::to_string),
            memory_type: memory_type.map(str::to_string),
            branch: branch.map(str::to_string),
            include_stale,
            limit,
            offset,
            fetch_limit: plan.fetch_limit,
            expanded_terms: plan.expanded_terms.clone(),
            core_terms: plan.core_terms.clone(),
            claim_terms: plan.claim_terms.clone(),
            fts_query: plan.fts_query.clone(),
            temporal_range: plan.temporal_range,
            temporal_field: plan.temporal_field.clone(),
            rrf_k: plan.weights.rrf_k,
            min_evidence_confidence: plan.weights.min_evidence_confidence,
            filtered_result_count,
            timings: plan.timings.clone(),
            rerank: None,
            channels,
            results,
            has_more: false,
            raw_fallback_count: 0,
        },
        contribution_breakdowns,
    })
}

#[allow(clippy::too_many_arguments)]
fn explain_result(
    memory: &Memory,
    index: usize,
    project: Option<&str>,
    plan: &QuerySearchPlan,
    id_to_fusion_score: &HashMap<i64, f64>,
    id_to_final_score: &HashMap<i64, f64>,
    staleness_labels: &HashMap<i64, crate::memory::MemoryStalenessLabel>,
    now_epoch: i64,
) -> Result<(SearchExplainResult, SearchExplainResultBreakdown)> {
    let final_score = id_to_final_score
        .get(&memory.id)
        .copied()
        .with_context(|| format!("missing final score for memory {}", memory.id))?;
    ensure!(
        final_score.is_finite() && final_score >= 0.0,
        "invalid final score {final_score} for memory {}",
        memory.id
    );
    let contributions = contributions_for(memory.id, plan)?;
    let contribution_sum = contributions
        .totals
        .iter()
        .try_fold(0.0, |total, contribution| {
            let next = total + contribution.score;
            ensure!(
                next.is_finite(),
                "fusion explanation score overflow for memory {}",
                memory.id
            );
            Ok(next)
        })?;
    let fusion_score = id_to_fusion_score
        .get(&memory.id)
        .copied()
        .with_context(|| format!("missing fusion score for memory {}", memory.id))?;
    ensure!(
        fusion_score.is_finite() && fusion_score > 0.0,
        "invalid fusion score {fusion_score} for memory {}",
        memory.id
    );
    let score_tolerance = f64::EPSILON * 16.0 * fusion_score.abs().max(1.0);
    ensure!(
        (fusion_score - contribution_sum).abs() <= score_tolerance,
        "fusion contribution mismatch for memory {}: fused={fusion_score} explained={contribution_sum}",
        memory.id
    );
    let post_fusion_score_factor = final_score / fusion_score;
    ensure!(
        post_fusion_score_factor.is_finite() && post_fusion_score_factor >= 0.0,
        "invalid post-fusion score factor for memory {}",
        memory.id
    );
    Ok((
        SearchExplainResult {
            memory_id: memory.id,
            final_rank: index + 1,
            final_score,
            evidence_confidence: candidate_confidence(memory, plan),
            project: memory.project.clone(),
            scope: memory.scope.clone(),
            visibility: visibility_label(memory, project).to_string(),
            staleness: staleness_labels
                .get(&memory.id)
                .cloned()
                .unwrap_or_else(|| memory::memory_staleness_label(memory, now_epoch)),
            contributions: contributions.totals,
        },
        SearchExplainResultBreakdown {
            memory_id: memory.id,
            contributions: contributions.breakdowns,
        },
    ))
}