Skip to main content

wm_memory/
lifecycle.rs

1//! Memory lifecycle — consolidation and mindful forgetting.
2//!
3//! Consolidation boosts importance of frequently-accessed memories.
4//! Mindful forgetting decays importance over time and removes memories
5//! that fall below a threshold.
6
7use crate::MemoryStore;
8
9use chrono::Utc;
10use uuid::Uuid;
11use wm_core::{Galaxy, Result};
12
13/// Configuration for the memory lifecycle.
14#[derive(Debug, Clone)]
15pub struct LifecycleConfig {
16    /// Importance threshold below which memories are candidates for forgetting.
17    pub forget_threshold: f32,
18    /// Decay factor applied per day since last access.
19    pub daily_decay_factor: f32,
20    /// Boost factor applied per access (consolidation).
21    pub access_boost: f32,
22    /// Maximum importance cap.
23    pub max_importance: f32,
24    /// Recency boost: memories accessed in the last N hours get a boost.
25    pub recency_window_hours: i64,
26    /// Recency boost amount.
27    pub recency_boost: f32,
28}
29
30impl Default for LifecycleConfig {
31    fn default() -> Self {
32        Self {
33            forget_threshold: 0.1,
34            daily_decay_factor: 0.95, // 5% decay per day
35            access_boost: 0.05,       // +5% per access
36            max_importance: 1.0,
37            recency_window_hours: 24,
38            recency_boost: 0.1,
39        }
40    }
41}
42
43/// Runs memory lifecycle operations on a `MemoryStore`.
44pub struct Lifecycle {
45    config: LifecycleConfig,
46}
47
48/// Result of a consolidation pass on a single galaxy.
49#[derive(Debug, Clone)]
50pub struct ConsolidationResult {
51    /// Galaxy processed
52    pub galaxy: Galaxy,
53    /// Memories examined
54    pub examined: usize,
55    /// Memories boosted
56    pub boosted: usize,
57    /// Memories decayed
58    pub decayed: usize,
59}
60
61/// Result of a mindful forgetting pass on a single galaxy.
62#[derive(Debug, Clone)]
63pub struct ForgettingResult {
64    /// Galaxy processed
65    pub galaxy: Galaxy,
66    /// Memories examined
67    pub examined: usize,
68    /// Memories forgotten (deleted)
69    pub forgotten: usize,
70    /// IDs of forgotten memories
71    pub forgotten_ids: Vec<Uuid>,
72}
73
74impl Lifecycle {
75    /// Create a new lifecycle manager with the given config.
76    #[must_use]
77    pub const fn new(config: LifecycleConfig) -> Self {
78        Self { config }
79    }
80
81    /// Create with default config.
82    #[must_use]
83    pub fn default_config() -> Self {
84        Self::new(LifecycleConfig::default())
85    }
86
87    /// Consolidate memories in a galaxy — boost frequently-accessed and recent memories.
88    pub fn consolidate(&self, store: &MemoryStore, galaxy: Galaxy) -> Result<ConsolidationResult> {
89        let memories = store.scan(galaxy, 10_000)?;
90        let now = Utc::now();
91        let mut examined = 0;
92        let mut boosted = 0;
93        let mut decayed = 0;
94
95        for mut mem in memories {
96            examined += 1;
97            let original_importance = mem.metadata.importance;
98
99            // Skip protected memories from consolidation changes
100            if mem.metadata.is_protected {
101                continue;
102            }
103
104            // Access-based boost
105            if mem.metadata.access_count > 0 {
106                let boost = self.config.access_boost * mem.metadata.access_count as f32;
107                mem.metadata.importance += boost;
108            }
109
110            // Recency boost
111            let hours_since_access = (now - mem.metadata.accessed_at).num_hours();
112            if hours_since_access < self.config.recency_window_hours {
113                mem.metadata.importance += self.config.recency_boost;
114            }
115
116            // Daily decay — uses per-memory half_life_days if configured
117            let days_since_access = (now - mem.metadata.accessed_at).num_days();
118            if days_since_access > 0 {
119                let decay = self
120                    .config
121                    .daily_decay_factor
122                    .powi(days_since_access as i32);
123                mem.metadata.importance *= decay;
124            }
125
126            // Apply neuro_score decay based on per-memory half-life
127            mem.decay(now);
128
129            // Clamp
130            mem.metadata.importance = mem
131                .metadata
132                .importance
133                .clamp(0.0, self.config.max_importance);
134
135            if mem.metadata.importance > original_importance {
136                boosted += 1;
137            } else if mem.metadata.importance < original_importance {
138                decayed += 1;
139            }
140
141            // Write back if changed
142            if (mem.metadata.importance - original_importance).abs() > f32::EPSILON {
143                store.put(galaxy, &mem)?;
144            }
145        }
146
147        Ok(ConsolidationResult {
148            galaxy,
149            examined,
150            boosted,
151            decayed,
152        })
153    }
154
155    /// Run mindful forgetting on a galaxy — delete memories below the importance threshold.
156    pub fn forget(&self, store: &MemoryStore, galaxy: Galaxy) -> Result<ForgettingResult> {
157        let memories = store.scan(galaxy, 10_000)?;
158        let mut examined = 0;
159        let mut forgotten = 0;
160        let mut forgotten_ids = Vec::new();
161
162        for mem in memories {
163            examined += 1;
164            if mem.should_forget(self.config.forget_threshold) {
165                let id = mem.metadata.id;
166                store.delete(galaxy, id)?;
167                forgotten += 1;
168                forgotten_ids.push(id);
169            }
170        }
171
172        Ok(ForgettingResult {
173            galaxy,
174            examined,
175            forgotten,
176            forgotten_ids,
177        })
178    }
179
180    /// Run a full lifecycle pass: consolidate then forget, across all galaxies.
181    pub fn run_full_cycle(
182        &self,
183        store: &MemoryStore,
184    ) -> Result<(Vec<ConsolidationResult>, Vec<ForgettingResult>)> {
185        let mut consol_results = Vec::new();
186        let mut forget_results = Vec::new();
187
188        for galaxy in wm_core::Galaxy::all() {
189            // Skip system galaxies
190            match galaxy {
191                Galaxy::Substrate | Galaxy::Dharma | Galaxy::Karma | Galaxy::Embeddings => continue,
192                _ => {}
193            }
194            let count = store.count(galaxy).unwrap_or(0);
195            if count == 0 {
196                continue;
197            }
198            consol_results.push(self.consolidate(store, galaxy)?);
199            forget_results.push(self.forget(store, galaxy)?);
200        }
201
202        Ok((consol_results, forget_results))
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::Memory;
210    use tempfile::tempdir;
211
212    fn test_store() -> MemoryStore {
213        let tmp = tempdir().unwrap();
214        MemoryStore::open_default(tmp.path()).unwrap()
215    }
216
217    #[test]
218    fn consolidate_boosts_accessed_memories() {
219        let store = test_store();
220        let galaxy = Galaxy::Codex;
221
222        // Create a memory and simulate access
223        let mut mem = Memory::new(galaxy, "frequently accessed".into());
224        mem.metadata.access_count = 5;
225        store.put(galaxy, &mem).unwrap();
226
227        let lifecycle = Lifecycle::default_config();
228        let result = lifecycle.consolidate(&store, galaxy).unwrap();
229
230        assert_eq!(result.examined, 1);
231        assert!(result.boosted >= 1);
232
233        // Verify importance was boosted
234        let retrieved = store.get(galaxy, mem.metadata.id).unwrap().unwrap();
235        assert!(retrieved.metadata.importance > 0.5);
236    }
237
238    #[test]
239    fn forget_removes_low_importance() {
240        let store = test_store();
241        let galaxy = Galaxy::Codex;
242
243        // Create a low-importance memory
244        let mem = Memory::new(galaxy, "unimportant".into()).with_importance(0.05);
245        store.put(galaxy, &mem).unwrap();
246
247        let lifecycle = Lifecycle::default_config();
248        let result = lifecycle.forget(&store, galaxy).unwrap();
249
250        assert_eq!(result.forgotten, 1);
251        assert!(store.get(galaxy, mem.metadata.id).unwrap().is_none());
252    }
253
254    #[test]
255    fn forget_keeps_important_memories() {
256        let store = test_store();
257        let galaxy = Galaxy::Codex;
258
259        let mem = Memory::new(galaxy, "important".into()).with_importance(0.9);
260        store.put(galaxy, &mem).unwrap();
261
262        let lifecycle = Lifecycle::default_config();
263        let result = lifecycle.forget(&store, galaxy).unwrap();
264
265        assert_eq!(result.forgotten, 0);
266        assert!(store.get(galaxy, mem.metadata.id).unwrap().is_some());
267    }
268
269    #[test]
270    fn full_cycle_processes_all_galaxies() {
271        let store = test_store();
272
273        // Add memories to a couple galaxies
274        let mem1 = Memory::new(Galaxy::Codex, "codex memory".into());
275        store.put(Galaxy::Codex, &mem1).unwrap();
276        let mem2 = Memory::new(Galaxy::Research, "research memory".into());
277        store.put(Galaxy::Research, &mem2).unwrap();
278
279        let lifecycle = Lifecycle::default_config();
280        let (consol, _forget) = lifecycle.run_full_cycle(&store).unwrap();
281
282        assert!(consol.iter().any(|c| c.galaxy == Galaxy::Codex));
283        assert!(consol.iter().any(|c| c.galaxy == Galaxy::Research));
284    }
285
286    #[test]
287    fn consolidate_skips_protected_memories() {
288        let store = test_store();
289        let galaxy = Galaxy::Codex;
290
291        let mut mem = Memory::new(galaxy, "protected".into());
292        mem.metadata.is_protected = true;
293        mem.metadata.importance = 0.5;
294        let id = mem.metadata.id;
295        store.put(galaxy, &mem).unwrap();
296
297        let lifecycle = Lifecycle::default_config();
298        let _result = lifecycle.consolidate(&store, galaxy).unwrap();
299
300        // Importance should be unchanged
301        let back = store.get(galaxy, id).unwrap().unwrap();
302        assert!((back.metadata.importance - 0.5).abs() < f32::EPSILON);
303    }
304
305    #[test]
306    fn forget_respects_is_protected() {
307        let store = test_store();
308        let galaxy = Galaxy::Codex;
309
310        let mem = Memory::new(galaxy, "protected low".into())
311            .with_importance(0.01)
312            .with_protection(true);
313        let id = mem.metadata.id;
314        store.put(galaxy, &mem).unwrap();
315
316        let lifecycle = Lifecycle::default_config();
317        let result = lifecycle.forget(&store, galaxy).unwrap();
318
319        assert_eq!(result.forgotten, 0);
320        assert!(store.get(galaxy, id).unwrap().is_some());
321    }
322}