use crate::MemoryStore;
use chrono::Utc;
use uuid::Uuid;
use wm_core::{Galaxy, Result};
#[derive(Debug, Clone)]
pub struct LifecycleConfig {
pub forget_threshold: f32,
pub daily_decay_factor: f32,
pub access_boost: f32,
pub max_importance: f32,
pub recency_window_hours: i64,
pub recency_boost: f32,
}
impl Default for LifecycleConfig {
fn default() -> Self {
Self {
forget_threshold: 0.1,
daily_decay_factor: 0.95, access_boost: 0.05, max_importance: 1.0,
recency_window_hours: 24,
recency_boost: 0.1,
}
}
}
pub struct Lifecycle {
config: LifecycleConfig,
}
#[derive(Debug, Clone)]
pub struct ConsolidationResult {
pub galaxy: Galaxy,
pub examined: usize,
pub boosted: usize,
pub decayed: usize,
}
#[derive(Debug, Clone)]
pub struct ForgettingResult {
pub galaxy: Galaxy,
pub examined: usize,
pub forgotten: usize,
pub forgotten_ids: Vec<Uuid>,
}
impl Lifecycle {
#[must_use]
pub const fn new(config: LifecycleConfig) -> Self {
Self { config }
}
#[must_use]
pub fn default_config() -> Self {
Self::new(LifecycleConfig::default())
}
pub fn consolidate(&self, store: &MemoryStore, galaxy: Galaxy) -> Result<ConsolidationResult> {
let memories = store.scan(galaxy, 10_000)?;
let now = Utc::now();
let mut examined = 0;
let mut boosted = 0;
let mut decayed = 0;
for mut mem in memories {
examined += 1;
let original_importance = mem.metadata.importance;
if mem.metadata.is_protected {
continue;
}
if mem.metadata.access_count > 0 {
let boost = self.config.access_boost * mem.metadata.access_count as f32;
mem.metadata.importance += boost;
}
let hours_since_access = (now - mem.metadata.accessed_at).num_hours();
if hours_since_access < self.config.recency_window_hours {
mem.metadata.importance += self.config.recency_boost;
}
let days_since_access = (now - mem.metadata.accessed_at).num_days();
if days_since_access > 0 {
let decay = self
.config
.daily_decay_factor
.powi(days_since_access as i32);
mem.metadata.importance *= decay;
}
mem.decay(now);
mem.metadata.importance = mem
.metadata
.importance
.clamp(0.0, self.config.max_importance);
if mem.metadata.importance > original_importance {
boosted += 1;
} else if mem.metadata.importance < original_importance {
decayed += 1;
}
if (mem.metadata.importance - original_importance).abs() > f32::EPSILON {
store.put(galaxy, &mem)?;
}
}
Ok(ConsolidationResult {
galaxy,
examined,
boosted,
decayed,
})
}
pub fn forget(&self, store: &MemoryStore, galaxy: Galaxy) -> Result<ForgettingResult> {
let memories = store.scan(galaxy, 10_000)?;
let mut examined = 0;
let mut forgotten = 0;
let mut forgotten_ids = Vec::new();
for mem in memories {
examined += 1;
if mem.should_forget(self.config.forget_threshold) {
let id = mem.metadata.id;
store.delete(galaxy, id)?;
forgotten += 1;
forgotten_ids.push(id);
}
}
Ok(ForgettingResult {
galaxy,
examined,
forgotten,
forgotten_ids,
})
}
pub fn run_full_cycle(
&self,
store: &MemoryStore,
) -> Result<(Vec<ConsolidationResult>, Vec<ForgettingResult>)> {
let mut consol_results = Vec::new();
let mut forget_results = Vec::new();
for galaxy in wm_core::Galaxy::all() {
match galaxy {
Galaxy::Substrate | Galaxy::Dharma | Galaxy::Karma | Galaxy::Embeddings => continue,
_ => {}
}
let count = store.count(galaxy).unwrap_or(0);
if count == 0 {
continue;
}
consol_results.push(self.consolidate(store, galaxy)?);
forget_results.push(self.forget(store, galaxy)?);
}
Ok((consol_results, forget_results))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Memory;
use tempfile::tempdir;
fn test_store() -> MemoryStore {
let tmp = tempdir().unwrap();
MemoryStore::open_default(tmp.path()).unwrap()
}
#[test]
fn consolidate_boosts_accessed_memories() {
let store = test_store();
let galaxy = Galaxy::Codex;
let mut mem = Memory::new(galaxy, "frequently accessed".into());
mem.metadata.access_count = 5;
store.put(galaxy, &mem).unwrap();
let lifecycle = Lifecycle::default_config();
let result = lifecycle.consolidate(&store, galaxy).unwrap();
assert_eq!(result.examined, 1);
assert!(result.boosted >= 1);
let retrieved = store.get(galaxy, mem.metadata.id).unwrap().unwrap();
assert!(retrieved.metadata.importance > 0.5);
}
#[test]
fn forget_removes_low_importance() {
let store = test_store();
let galaxy = Galaxy::Codex;
let mem = Memory::new(galaxy, "unimportant".into()).with_importance(0.05);
store.put(galaxy, &mem).unwrap();
let lifecycle = Lifecycle::default_config();
let result = lifecycle.forget(&store, galaxy).unwrap();
assert_eq!(result.forgotten, 1);
assert!(store.get(galaxy, mem.metadata.id).unwrap().is_none());
}
#[test]
fn forget_keeps_important_memories() {
let store = test_store();
let galaxy = Galaxy::Codex;
let mem = Memory::new(galaxy, "important".into()).with_importance(0.9);
store.put(galaxy, &mem).unwrap();
let lifecycle = Lifecycle::default_config();
let result = lifecycle.forget(&store, galaxy).unwrap();
assert_eq!(result.forgotten, 0);
assert!(store.get(galaxy, mem.metadata.id).unwrap().is_some());
}
#[test]
fn full_cycle_processes_all_galaxies() {
let store = test_store();
let mem1 = Memory::new(Galaxy::Codex, "codex memory".into());
store.put(Galaxy::Codex, &mem1).unwrap();
let mem2 = Memory::new(Galaxy::Research, "research memory".into());
store.put(Galaxy::Research, &mem2).unwrap();
let lifecycle = Lifecycle::default_config();
let (consol, _forget) = lifecycle.run_full_cycle(&store).unwrap();
assert!(consol.iter().any(|c| c.galaxy == Galaxy::Codex));
assert!(consol.iter().any(|c| c.galaxy == Galaxy::Research));
}
#[test]
fn consolidate_skips_protected_memories() {
let store = test_store();
let galaxy = Galaxy::Codex;
let mut mem = Memory::new(galaxy, "protected".into());
mem.metadata.is_protected = true;
mem.metadata.importance = 0.5;
let id = mem.metadata.id;
store.put(galaxy, &mem).unwrap();
let lifecycle = Lifecycle::default_config();
let _result = lifecycle.consolidate(&store, galaxy).unwrap();
let back = store.get(galaxy, id).unwrap().unwrap();
assert!((back.metadata.importance - 0.5).abs() < f32::EPSILON);
}
#[test]
fn forget_respects_is_protected() {
let store = test_store();
let galaxy = Galaxy::Codex;
let mem = Memory::new(galaxy, "protected low".into())
.with_importance(0.01)
.with_protection(true);
let id = mem.metadata.id;
store.put(galaxy, &mem).unwrap();
let lifecycle = Lifecycle::default_config();
let result = lifecycle.forget(&store, galaxy).unwrap();
assert_eq!(result.forgotten, 0);
assert!(store.get(galaxy, id).unwrap().is_some());
}
}