1use crate::store::MemoryStore;
2
3const DEFAULT_DECAY_FACTOR: f64 = 0.5;
5
6const DEFAULT_HALF_LIFE_HOURS: f64 = 24.0;
8
9const MIN_STRENGTH: f64 = 0.01;
11
12#[derive(Debug, Clone, serde::Serialize)]
14pub struct DecayResult {
15 pub decayed: usize,
16 pub deleted: usize,
17}
18
19pub fn run_decay(
27 store: &MemoryStore,
28 decay_factor: Option<f64>,
29 half_life_hours: Option<f64>,
30) -> Result<DecayResult, rusqlite::Error> {
31 run_decay_ns(store, decay_factor, half_life_hours, "default")
32}
33
34pub fn run_decay_ns(
35 store: &MemoryStore,
36 decay_factor: Option<f64>,
37 half_life_hours: Option<f64>,
38 namespace: &str,
39) -> Result<DecayResult, rusqlite::Error> {
40 let factor = decay_factor.unwrap_or(DEFAULT_DECAY_FACTOR);
41 let half_life = half_life_hours.unwrap_or(DEFAULT_HALF_LIFE_HOURS);
42
43 let decayed = store.decay_all_ns(factor, half_life, namespace)?;
44
45 let deleted: usize = store.conn().execute(
47 "DELETE FROM memories WHERE strength < ?1 AND namespace = ?2",
48 rusqlite::params![MIN_STRENGTH, namespace],
49 )?;
50
51 if deleted > 0 {
52 store.log_audit("decay_delete", None, "system", Some(&format!("{{\"deleted\":{deleted},\"namespace\":{}}}", serde_json::json!(namespace))))?;
53 }
54
55 Ok(DecayResult { decayed, deleted })
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn test_decay_pass() {
64 let store = MemoryStore::open_in_memory().unwrap();
65 let id = store.remember_fact("A", "is", "B", None).unwrap();
66
67 store.update_importance(id, 0.0).unwrap();
69
70 let old_time = (chrono::Utc::now() - chrono::Duration::hours(48)).to_rfc3339();
72 store
73 .conn()
74 .execute(
75 "UPDATE memories SET last_accessed_at = ?1",
76 rusqlite::params![old_time],
77 )
78 .unwrap();
79
80 let result = run_decay(&store, None, None).unwrap();
81 assert_eq!(result.decayed, 1);
82
83 let mem = store.get_memory(1).unwrap().unwrap();
85 assert!(mem.strength < 0.3);
86 assert!(mem.strength > 0.2);
87 }
88
89 #[test]
90 fn test_no_decay_for_fresh_memories() {
91 let store = MemoryStore::open_in_memory().unwrap();
92 store.remember_fact("A", "is", "B", None).unwrap();
93
94 let result = run_decay(&store, None, None).unwrap();
95 assert_eq!(result.decayed, 0);
96 assert_eq!(result.deleted, 0);
97
98 let mem = store.get_memory(1).unwrap().unwrap();
99 assert!((mem.strength - 1.0).abs() < 0.01);
100 }
101
102 #[test]
103 fn test_decay_respects_importance() {
104 let store = MemoryStore::open_in_memory().unwrap();
105 let id_low = store.remember_fact("low", "importance", "mem", None).unwrap();
106 let id_high = store.remember_fact("high", "importance", "mem", None).unwrap();
107
108 store.update_importance(id_low, 0.0).unwrap();
110 store.update_importance(id_high, 1.0).unwrap();
111
112 let old_time = (chrono::Utc::now() - chrono::Duration::hours(48)).to_rfc3339();
114 store
115 .conn()
116 .execute(
117 "UPDATE memories SET last_accessed_at = ?1",
118 rusqlite::params![old_time],
119 )
120 .unwrap();
121
122 let _result = run_decay(&store, None, None).unwrap();
123
124 let low = store.get_memory(id_low).unwrap().unwrap();
125 let high = store.get_memory(id_high).unwrap().unwrap();
126
127 assert!(
129 high.strength > low.strength,
130 "high importance ({:.4}) should decay slower than low importance ({:.4})",
131 high.strength, low.strength
132 );
133 }
134
135 #[test]
136 fn test_decay_deletes_very_weak() {
137 let store = MemoryStore::open_in_memory().unwrap();
138 store.remember_fact("A", "is", "B", None).unwrap();
139
140 let old_time = (chrono::Utc::now() - chrono::Duration::hours(24 * 30)).to_rfc3339();
142 store
143 .conn()
144 .execute(
145 "UPDATE memories SET strength = 0.001, last_accessed_at = ?1",
146 rusqlite::params![old_time],
147 )
148 .unwrap();
149
150 let result = run_decay(&store, None, None).unwrap();
151 assert!(result.deleted > 0);
152
153 let mem = store.get_memory(1).unwrap();
155 assert!(mem.is_none());
156 }
157}