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