axiomsync 1.0.1

Local retrieval runtime and CLI for AxiomSync.
Documentation
use std::collections::HashMap;

use crate::embedding::embed_text;
use crate::error::{AxiomError, Result};

use super::super::memory_extractor::ExtractedMemory;
use super::Session;
use super::dedup::{prefilter_existing_memory_matches, resolve_dedup_selection};
use super::fallbacks::record_memory_dedup_fallback as record_memory_dedup_fallback_event;
use super::helpers::{build_memory_key, normalize_memory_text};
use super::promotion::memory_uri_for_category_key;
use super::read_path::list_existing_memory_facts;
use super::types::{
    ExistingMemoryFact, MemoryDedupConfig, MemoryDedupDecision, ResolvedMemoryCandidate,
};

impl Session {
    pub(super) fn resolve_memory_candidates(
        &self,
        extracted: &[ExtractedMemory],
    ) -> Result<Vec<ResolvedMemoryCandidate>> {
        let mut by_category = HashMap::<String, Vec<ExistingMemoryFact>>::new();
        let mut resolved = Vec::<ResolvedMemoryCandidate>::new();
        let dedup_config = MemoryDedupConfig::from_snapshot(&self.config.memory.dedup);
        let mut dedup_fallback_logged = false;

        for candidate in extracted {
            let normalized_text = normalize_memory_text(&candidate.text);
            if normalized_text.is_empty() || candidate.source_message_ids.is_empty() {
                continue;
            }

            if !by_category.contains_key(&candidate.category) {
                let existing = list_existing_memory_facts(self, &candidate.category)?;
                by_category.insert(candidate.category.clone(), existing);
            }
            let existing = by_category
                .get_mut(&candidate.category)
                .ok_or_else(|| AxiomError::Internal("memory category cache missing".to_string()))?;

            let prefiltered = prefilter_existing_memory_matches(
                &normalized_text,
                existing,
                dedup_config.similarity_threshold,
            );
            let (selection, llm_error) =
                resolve_dedup_selection(candidate, &normalized_text, &prefiltered, &dedup_config)?;
            if let Some(error) = llm_error
                && !dedup_fallback_logged
            {
                record_memory_dedup_fallback_event(self, dedup_config.mode.as_str(), &error);
                dedup_fallback_logged = true;
            }

            if selection.decision == MemoryDedupDecision::Skip {
                continue;
            }

            let selected_match = selection
                .selected_index
                .and_then(|index| prefiltered.get(index));
            let (target_uri, canonical_text) = selected_match.map_or_else(
                || (None, normalized_text.clone()),
                |found| (Some(found.uri.clone()), found.text.clone()),
            );
            let key = build_memory_key(&candidate.category, &canonical_text);
            let key_for_future = key.clone();
            let source_message_ids = dedup_source_ids(&candidate.source_message_ids);

            merge_resolved_candidate(
                &mut resolved,
                ResolvedMemoryCandidate {
                    category: candidate.category.clone(),
                    key,
                    text: canonical_text.clone(),
                    source_message_ids,
                    target_uri: target_uri.clone(),
                },
            );

            if target_uri.is_none() {
                let future_uri = memory_uri_for_category_key(&candidate.category, &key_for_future)?;
                existing.push(ExistingMemoryFact {
                    uri: future_uri,
                    text: canonical_text.clone(),
                    vector: embed_text(&canonical_text),
                });
            }
        }

        Ok(resolved)
    }
}

pub(super) fn merge_resolved_candidate(
    resolved: &mut Vec<ResolvedMemoryCandidate>,
    mut next: ResolvedMemoryCandidate,
) {
    next.source_message_ids = dedup_source_ids(&next.source_message_ids);
    if let Some(existing) = resolved.iter_mut().find(|item| {
        item.category == next.category
            && item.text == next.text
            && item.target_uri == next.target_uri
    }) {
        existing
            .source_message_ids
            .extend(next.source_message_ids.clone());
        existing.source_message_ids = dedup_source_ids(&existing.source_message_ids);
    } else {
        resolved.push(next);
    }
}

pub(super) fn dedup_source_ids(ids: &[String]) -> Vec<String> {
    let mut out = ids
        .iter()
        .map(|id| id.trim())
        .filter(|id| !id.is_empty())
        .map(ToString::to_string)
        .collect::<Vec<_>>();
    out.sort();
    out.dedup();
    out
}