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 pub fn phagic_digest(
211 &self,
212 store: &MemoryStore,
213 galaxy: Galaxy,
214 ) -> Result<crate::cold_storage::PhagicDigestReport> {
215 let digester = crate::cold_storage::PhagicDigester::default_config();
216 digester.digest_galaxy(store, None, galaxy)
217 }
218
219 pub fn run_full_cycle_phagic(
222 &self,
223 store: &MemoryStore,
224 ) -> Result<(
225 Vec<ConsolidationResult>,
226 Vec<crate::cold_storage::PhagicDigestReport>,
227 )> {
228 let mut consol_results = Vec::new();
229 let mut phagic_results = Vec::new();
230
231 for galaxy in wm_core::Galaxy::all() {
232 match galaxy {
234 Galaxy::Substrate
235 | Galaxy::Dharma
236 | Galaxy::Karma
237 | Galaxy::Embeddings
238 | Galaxy::Associations => continue,
239 _ => {}
240 }
241 let count = store.count(galaxy).unwrap_or(0);
242 if count == 0 {
243 continue;
244 }
245 consol_results.push(self.consolidate(store, galaxy)?);
246 phagic_results.push(self.phagic_digest(store, galaxy)?);
247 }
248
249 Ok((consol_results, phagic_results))
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::Memory;
257 use tempfile::tempdir;
258
259 fn test_store() -> MemoryStore {
260 let tmp = tempdir().unwrap();
261 MemoryStore::open_default(tmp.path()).unwrap()
262 }
263
264 #[test]
265 fn consolidate_boosts_accessed_memories() {
266 let store = test_store();
267 let galaxy = Galaxy::Codex;
268
269 let mut mem = Memory::new(galaxy, "frequently accessed".into());
271 mem.metadata.access_count = 5;
272 store.put(galaxy, &mem).unwrap();
273
274 let lifecycle = Lifecycle::default_config();
275 let result = lifecycle.consolidate(&store, galaxy).unwrap();
276
277 assert_eq!(result.examined, 1);
278 assert!(result.boosted >= 1);
279
280 let retrieved = store.get(galaxy, mem.metadata.id).unwrap().unwrap();
282 assert!(retrieved.metadata.importance > 0.5);
283 }
284
285 #[test]
286 fn forget_removes_low_importance() {
287 let store = test_store();
288 let galaxy = Galaxy::Codex;
289
290 let mem = Memory::new(galaxy, "unimportant".into()).with_importance(0.05);
292 store.put(galaxy, &mem).unwrap();
293
294 let lifecycle = Lifecycle::default_config();
295 let result = lifecycle.forget(&store, galaxy).unwrap();
296
297 assert_eq!(result.forgotten, 1);
298 assert!(store.get(galaxy, mem.metadata.id).unwrap().is_none());
299 }
300
301 #[test]
302 fn forget_keeps_important_memories() {
303 let store = test_store();
304 let galaxy = Galaxy::Codex;
305
306 let mem = Memory::new(galaxy, "important".into()).with_importance(0.9);
307 store.put(galaxy, &mem).unwrap();
308
309 let lifecycle = Lifecycle::default_config();
310 let result = lifecycle.forget(&store, galaxy).unwrap();
311
312 assert_eq!(result.forgotten, 0);
313 assert!(store.get(galaxy, mem.metadata.id).unwrap().is_some());
314 }
315
316 #[test]
317 fn full_cycle_processes_all_galaxies() {
318 let store = test_store();
319
320 let mem1 = Memory::new(Galaxy::Codex, "codex memory".into());
322 store.put(Galaxy::Codex, &mem1).unwrap();
323 let mem2 = Memory::new(Galaxy::Research, "research memory".into());
324 store.put(Galaxy::Research, &mem2).unwrap();
325
326 let lifecycle = Lifecycle::default_config();
327 let (consol, _forget) = lifecycle.run_full_cycle(&store).unwrap();
328
329 assert!(consol.iter().any(|c| c.galaxy == Galaxy::Codex));
330 assert!(consol.iter().any(|c| c.galaxy == Galaxy::Research));
331 }
332
333 #[test]
334 fn consolidate_skips_protected_memories() {
335 let store = test_store();
336 let galaxy = Galaxy::Codex;
337
338 let mut mem = Memory::new(galaxy, "protected".into());
339 mem.metadata.is_protected = true;
340 mem.metadata.importance = 0.5;
341 let id = mem.metadata.id;
342 store.put(galaxy, &mem).unwrap();
343
344 let lifecycle = Lifecycle::default_config();
345 let _result = lifecycle.consolidate(&store, galaxy).unwrap();
346
347 let back = store.get(galaxy, id).unwrap().unwrap();
349 assert!((back.metadata.importance - 0.5).abs() < f32::EPSILON);
350 }
351
352 #[test]
353 fn forget_respects_is_protected() {
354 let store = test_store();
355 let galaxy = Galaxy::Codex;
356
357 let mem = Memory::new(galaxy, "protected low".into())
358 .with_importance(0.01)
359 .with_protection(true);
360 let id = mem.metadata.id;
361 store.put(galaxy, &mem).unwrap();
362
363 let lifecycle = Lifecycle::default_config();
364 let result = lifecycle.forget(&store, galaxy).unwrap();
365
366 assert_eq!(result.forgotten, 0);
367 assert!(store.get(galaxy, id).unwrap().is_some());
368 }
369
370 #[test]
371 fn phagic_lifecycle_non_destructive_preservation() {
372 let store = test_store();
373 let galaxy = Galaxy::Codex;
374 let now = Utc::now();
375
376 let mut mem = Memory::new(galaxy, "Outer-rim lifecycle candidate".into())
377 .with_importance(0.02)
378 .with_tags(vec!["obsolete".into()]);
379 mem.metadata.accessed_at = now - chrono::Duration::days(160);
380 let id = mem.metadata.id;
381 store.put(galaxy, &mem).unwrap();
382
383 let lifecycle = Lifecycle::default_config();
384 let report = lifecycle.phagic_digest(&store, galaxy).unwrap();
385
386 assert_eq!(report.memories_digested, 1);
387 assert!(store.get(galaxy, id).unwrap().is_none());
389 let cold = store.get_cold_record(id).unwrap();
391 assert!(cold.is_some());
392 assert_eq!(cold.unwrap().id, id);
393 }
394}