Skip to main content

codemem_engine/consolidation/
forget.rs

1use super::ConsolidationResult;
2use crate::CodememEngine;
3use codemem_core::{CodememError, GraphBackend, VectorBackend};
4use serde_json::json;
5
6impl CodememEngine {
7    /// Consolidate forget: delete low-importance, never-accessed memories.
8    pub fn consolidate_forget(
9        &self,
10        importance_threshold: Option<f64>,
11        target_tags: Option<&[String]>,
12        max_access_count: Option<u32>,
13    ) -> Result<ConsolidationResult, CodememError> {
14        let importance_threshold = importance_threshold.unwrap_or(0.1);
15        let max_access_count = max_access_count.unwrap_or(0);
16
17        // M13: When max_access_count > 0 (non-default), filter in Rust since
18        // find_forgettable() hardcodes access_count = 0 in SQL.
19        let ids = match target_tags {
20            Some(tags) if !tags.is_empty() => {
21                self.find_forgettable_by_tags(importance_threshold, tags, max_access_count)?
22            }
23            _ if max_access_count > 0 => {
24                // find_forgettable only returns access_count=0; fall back to manual filtering
25                let all = self.storage.list_memories_filtered(None, None)?;
26                all.into_iter()
27                    .filter(|m| {
28                        m.importance < importance_threshold && m.access_count <= max_access_count
29                    })
30                    .map(|m| m.id)
31                    .collect()
32            }
33            _ => self.storage.find_forgettable(importance_threshold)?,
34        };
35
36        let deleted = ids.len();
37
38        // H2: Batch deletes in groups of 100, releasing all locks between batches.
39        // SQLite cascade (memory + graph nodes/edges + embeddings) is batched into
40        // a single transaction per chunk; in-memory indices are updated afterwards.
41        for batch in ids.chunks(100) {
42            let batch_refs: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
43            if let Err(e) = self.storage.delete_memories_batch_cascade(&batch_refs) {
44                tracing::warn!(
45                    "Failed to batch-delete {} memories during forget consolidation: {e}",
46                    batch.len()
47                );
48            }
49
50            // C1: Lock ordering: graph first, then vector, then bm25
51            let mut graph = self.lock_graph()?;
52            let mut vector = self.lock_vector()?;
53            let mut bm25 = self.lock_bm25()?;
54            for id in batch {
55                if let Err(e) = graph.remove_node(id) {
56                    tracing::warn!(
57                        "Failed to remove {id} from graph during forget consolidation: {e}"
58                    );
59                }
60                if let Err(e) = vector.remove(id) {
61                    tracing::warn!(
62                        "Failed to remove {id} from vector index during forget consolidation: {e}"
63                    );
64                }
65                bm25.remove_document(id);
66            }
67            drop(bm25);
68            drop(vector);
69            drop(graph);
70        }
71
72        // Rebuild vector index if we deleted anything
73        if deleted > 0 {
74            let mut vector = self.lock_vector()?;
75            self.rebuild_vector_index_internal(&mut vector);
76            drop(vector);
77        }
78
79        self.save_index();
80
81        if let Err(e) = self.storage.insert_consolidation_log("forget", deleted) {
82            tracing::warn!("Failed to log forget consolidation: {e}");
83        }
84
85        Ok(ConsolidationResult {
86            cycle: "forget".to_string(),
87            affected: deleted,
88            details: json!({
89                "threshold": importance_threshold,
90            }),
91        })
92    }
93}