1use crate::MemoryStore;
8
9use chrono::Utc;
10use uuid::Uuid;
11use wm_core::{Galaxy, Result};
12
13#[derive(Debug, Clone)]
15pub struct LifecycleConfig {
16 pub forget_threshold: f32,
18 pub daily_decay_factor: f32,
20 pub access_boost: f32,
22 pub max_importance: f32,
24 pub recency_window_hours: i64,
26 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, access_boost: 0.05, max_importance: 1.0,
37 recency_window_hours: 24,
38 recency_boost: 0.1,
39 }
40 }
41}
42
43pub struct Lifecycle {
45 config: LifecycleConfig,
46}
47
48#[derive(Debug, Clone)]
50pub struct ConsolidationResult {
51 pub galaxy: Galaxy,
53 pub examined: usize,
55 pub boosted: usize,
57 pub decayed: usize,
59}
60
61#[derive(Debug, Clone)]
63pub struct ForgettingResult {
64 pub galaxy: Galaxy,
66 pub examined: usize,
68 pub forgotten: usize,
70 pub forgotten_ids: Vec<Uuid>,
72}
73
74impl Lifecycle {
75 #[must_use]
77 pub const fn new(config: LifecycleConfig) -> Self {
78 Self { config }
79 }
80
81 #[must_use]
83 pub fn default_config() -> Self {
84 Self::new(LifecycleConfig::default())
85 }
86
87 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 if mem.metadata.is_protected {
101 continue;
102 }
103
104 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 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 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 mem.decay(now);
128
129 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 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 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 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 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 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 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 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 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 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}