Skip to main content

codemem_engine/consolidation/
mod.rs

1//! Consolidation logic for the Codemem memory engine.
2//!
3//! Contains all 5 consolidation cycles (decay, creative, cluster, forget, summarize),
4//! helper data structures (UnionFind), and consolidation status queries.
5//!
6//! Lock ordering: always graph -> vector -> bm25 (to prevent deadlocks).
7
8mod cluster;
9mod creative;
10mod decay;
11mod forget;
12mod summarize;
13pub mod union_find;
14
15pub use union_find::UnionFind;
16
17use crate::CodememEngine;
18use codemem_core::{CodememError, VectorBackend};
19
20/// Result of a consolidation cycle.
21pub struct ConsolidationResult {
22    /// Name of the cycle (decay, creative, cluster, forget, summarize).
23    pub cycle: String,
24    /// Number of affected items (meaning depends on cycle type).
25    pub affected: usize,
26    /// Additional details as JSON.
27    pub details: serde_json::Value,
28}
29
30/// Status of a single consolidation cycle.
31pub struct ConsolidationStatusEntry {
32    pub cycle_type: String,
33    pub last_run: String,
34    pub affected_count: usize,
35}
36
37impl CodememEngine {
38    /// Get the status of all consolidation cycles.
39    pub fn consolidation_status(&self) -> Result<Vec<ConsolidationStatusEntry>, CodememError> {
40        let runs = self.storage.last_consolidation_runs()?;
41        let mut entries = Vec::new();
42        for entry in &runs {
43            let dt = chrono::DateTime::from_timestamp(entry.run_at, 0)
44                .map(|t| t.format("%Y-%m-%d %H:%M:%S UTC").to_string())
45                .unwrap_or_else(|| "unknown".to_string());
46            entries.push(ConsolidationStatusEntry {
47                cycle_type: entry.cycle_type.clone(),
48                last_run: dt,
49                affected_count: entry.affected_count,
50            });
51        }
52        Ok(entries)
53    }
54
55    /// Find memories matching any of the target tags below importance threshold
56    /// and with access_count <= max_access_count.
57    ///
58    /// M14: Note — this loads all memories and filters in Rust. For large databases,
59    /// a storage method with SQL WHERE clauses for importance/access_count/tags would
60    /// be more efficient, but that requires adding a new StorageBackend trait method.
61    pub fn find_forgettable_by_tags(
62        &self,
63        importance_threshold: f64,
64        target_tags: &[String],
65        max_access_count: u32,
66    ) -> Result<Vec<String>, CodememError> {
67        let all_memories = self.storage.list_memories_filtered(None, None)?;
68        let mut forgettable = Vec::new();
69
70        for memory in &all_memories {
71            if memory.importance >= importance_threshold {
72                continue;
73            }
74            if memory.access_count > max_access_count {
75                continue;
76            }
77            if memory.tags.iter().any(|t| target_tags.contains(t)) {
78                forgettable.push(memory.id.clone());
79            }
80        }
81
82        Ok(forgettable)
83    }
84
85    /// Internal helper: rebuild vector index from all stored embeddings.
86    pub fn rebuild_vector_index_internal(&self, vector: &mut codemem_storage::HnswIndex) {
87        let embeddings = match self.storage.list_all_embeddings() {
88            Ok(e) => e,
89            Err(e) => {
90                tracing::warn!("Failed to rebuild vector index: {e}");
91                return;
92            }
93        };
94
95        if let Ok(mut fresh) = codemem_storage::HnswIndex::with_defaults() {
96            for (id, floats) in &embeddings {
97                let _ = fresh.insert(id, floats);
98            }
99            *vector = fresh;
100        }
101    }
102}