remem-ai 0.6.11

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

use anyhow::Result;
use rusqlite::Connection;

use crate::perf::time_result;
use crate::retrieval::search::common::{rank_normalized_score, WeightedRankedHit};

use super::super::{suppression_filter, SearchWeights};
use super::NamedChannel;

#[allow(clippy::too_many_arguments)]
pub(super) fn append_graph_channel(
    conn: &Connection,
    channels: &mut Vec<NamedChannel>,
    timings: &mut Vec<crate::perf::PhaseTiming>,
    project: Option<&str>,
    memory_type: Option<&str>,
    branch: Option<&str>,
    include_stale: bool,
    include_suppressed: bool,
    fetch_limit: i64,
    weights: SearchWeights,
) -> Result<()> {
    if weights.graph <= 0.0 {
        channels.push(NamedChannel::disabled(
            "graph_traversal",
            weights.graph,
            "graph channel weight is zero",
        ));
        return Ok(());
    }

    let seed_ids = seed_ids(channels, 32);
    let outcome = time_result(timings, "graph_traversal", || {
        crate::retrieval::graph::traverse_trusted_graph(
            conn,
            crate::retrieval::graph::GraphTraversalRequest {
                seed_memory_ids: &seed_ids,
                project,
                memory_type,
                branch,
                include_inactive: include_stale,
                reference_time_epoch: chrono::Utc::now().timestamp(),
                limits: crate::retrieval::graph::GraphTraversalLimits::for_search(fetch_limit),
            },
        )
    })?;
    if let Some(reason) = outcome.status.disabled_reason() {
        channels.push(
            NamedChannel::disabled("graph_traversal", weights.graph, reason)
                .with_candidates_scanned(outcome.diagnostics.edges_scanned),
        );
        return Ok(());
    }

    let hits = outcome
        .hits
        .into_iter()
        .enumerate()
        .map(|(rank, hit)| WeightedRankedHit {
            id: hit.memory_id,
            normalized_score: rank_normalized_score(rank),
        })
        .collect::<Vec<_>>();
    let hits = suppression_filter::weighted_hits(conn, hits, include_suppressed)?;
    channels.push(graph_channel_after_suppression(
        hits,
        weights.graph,
        outcome.diagnostics.edges_scanned,
    ));
    Ok(())
}

fn graph_channel_after_suppression(
    hits: Vec<WeightedRankedHit>,
    weight: f64,
    candidates_scanned: usize,
) -> NamedChannel {
    if hits.is_empty() {
        return NamedChannel::disabled(
            "graph_traversal",
            weight,
            "all eligible graph candidates were suppressed",
        )
        .with_candidates_scanned(candidates_scanned);
    }
    NamedChannel::enabled_with_hits("graph_traversal", weight, hits)
        .with_candidates_scanned(candidates_scanned)
}

fn seed_ids(channels: &[NamedChannel], max_seeds: usize) -> Vec<i64> {
    let mut seen = HashSet::new();
    channels
        .iter()
        .filter(|channel| channel.name == "fts" || channel.name == "vector")
        .flat_map(|channel| channel.hits.iter().map(|hit| hit.id))
        .filter(|id| seen.insert(*id))
        .take(max_seeds)
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn post_suppression_empty_graph_channel_has_stable_disabled_reason() {
        let channel =
            graph_channel_after_suppression(Vec::new(), SearchWeights::default().graph, 2);

        assert!(!channel.is_enabled());
        assert!(channel.hits.is_empty());
        assert_eq!(channel.candidates_scanned, Some(2));
        assert_eq!(
            channel.disabled_reason.as_deref(),
            Some("all eligible graph candidates were suppressed")
        );
    }
}