oxios_memory/memory/
decay.rs1use chrono::{DateTime, Utc};
7
8use crate::memory::types::{MemoryEntry, ProtectionLevel};
9
10#[derive(Debug, Clone)]
22pub struct DecayEngine {
23 pub multiplier: f32,
25}
26
27impl DecayEngine {
28 pub fn new(multiplier: f32) -> Self {
30 Self { multiplier }
31 }
32
33 pub fn default_engine() -> Self {
35 Self::new(1.0)
36 }
37
38 pub fn compute_decay(&self, entry: &MemoryEntry, now: DateTime<Utc>) -> f32 {
43 if entry.pinned || entry.protection == ProtectionLevel::Permanent {
45 return 1.0;
46 }
47
48 let hours_since_access = (now - entry.accessed_at).num_hours().max(0) as f32;
49 let base_rate = entry.memory_type.base_decay_rate();
50
51 let access_boost = 1.0 + (1.0_f32 + entry.access_count as f32).ln();
53
54 let protection_mult = entry.protection.decay_multiplier();
56
57 let effective_rate = base_rate * self.multiplier * protection_mult / access_boost;
58 let retention = (-effective_rate * hours_since_access).exp();
59 retention.clamp(0.0, 1.0)
60 }
61
62 pub fn effective_importance(entry: &MemoryEntry) -> f32 {
66 let access_boost = 1.0 + (1.0_f32 + entry.access_count as f32).ln();
67 entry.importance * access_boost * entry.decay_score
68 }
69
70 pub fn is_prunable(&self, entry: &MemoryEntry, threshold: f32) -> bool {
78 if entry.pinned {
79 return false;
80 }
81 if entry.protection >= super::ProtectionLevel::Medium {
82 return false;
83 }
84 if entry.memory_type.is_auto_protected() {
85 return false;
86 }
87 entry.decay_score < threshold
88 }
89}
90
91#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::memory::{MemoryEntry, MemoryType, ProtectionLevel};
99 use chrono::Duration;
100
101 fn make_entry(hours_ago: i64) -> MemoryEntry {
102 MemoryEntry {
103 id: "test".to_string(),
104 memory_type: MemoryType::Fact,
105 tier: crate::memory::MemoryTier::Warm,
106 content: "test content".to_string(),
107 content_hash: 0,
108 tags: vec![],
109 source: "test".to_string(),
110 session_id: None,
111 importance: 0.5,
112 pinned: false,
113 protection: ProtectionLevel::None,
114 auto_classified: false,
115 session_appearances: 0,
116 user_corrected: false,
117 seen_in_sessions: vec![],
118 created_at: Utc::now(),
119 accessed_at: Utc::now() - Duration::hours(hours_ago),
120 modified_at: Utc::now(),
121 access_count: 0,
122 decay_score: 1.0,
123 compaction_level: 0,
124 compacted_from: vec![],
125 related_ids: vec![],
126 contradicts: None,
127 }
128 }
129
130 #[test]
131 fn test_decay_fresh() {
132 let engine = DecayEngine::new(1.0);
133 let entry = make_entry(0); let score = engine.compute_decay(&entry, Utc::now());
135 assert!(
136 score > 0.99,
137 "Fresh entry should have decay ~1.0, got {}",
138 score
139 );
140 }
141
142 #[test]
143 fn test_decay_old() {
144 let engine = DecayEngine::new(1.0);
145 let entry = make_entry(720); let score = engine.compute_decay(&entry, Utc::now());
147 assert!(
148 score < 0.5,
149 "Old entry should have significant decay, got {}",
150 score
151 );
152 }
153
154 #[test]
155 fn test_decay_permanent_protection() {
156 let engine = DecayEngine::new(1.0);
157 let mut entry = make_entry(720);
158 entry.protection = ProtectionLevel::Permanent;
159 let score = engine.compute_decay(&entry, Utc::now());
160 assert_eq!(score, 1.0, "Permanent protection should always be 1.0");
161 }
162
163 #[test]
164 fn test_decay_pinned() {
165 let engine = DecayEngine::new(1.0);
166 let mut entry = make_entry(720);
167 entry.pinned = true;
168 let score = engine.compute_decay(&entry, Utc::now());
169 assert_eq!(score, 1.0, "Pinned entry should always be 1.0");
170 }
171
172 #[test]
173 fn test_decay_high_protection_slower() {
174 let engine = DecayEngine::new(1.0);
175 let mut entry_none = make_entry(168); entry_none.protection = ProtectionLevel::None;
177
178 let mut entry_high = make_entry(168);
179 entry_high.protection = ProtectionLevel::High;
180
181 let score_none = engine.compute_decay(&entry_none, Utc::now());
182 let score_high = engine.compute_decay(&entry_high, Utc::now());
183 assert!(
184 score_high > score_none,
185 "High protection should decay slower (high={}, none={})",
186 score_high,
187 score_none
188 );
189 }
190
191 #[test]
192 fn test_decay_access_boost() {
193 let engine = DecayEngine::new(1.0);
194 let mut entry_low = make_entry(168);
195 entry_low.access_count = 0;
196
197 let mut entry_high = make_entry(168);
198 entry_high.access_count = 10;
199
200 let score_low = engine.compute_decay(&entry_low, Utc::now());
201 let score_high = engine.compute_decay(&entry_high, Utc::now());
202 assert!(
203 score_high > score_low,
204 "Frequently accessed should decay slower (high={}, low={})",
205 score_high,
206 score_low
207 );
208 }
209
210 #[test]
211 fn test_effective_importance() {
212 let mut entry = make_entry(0);
213 entry.importance = 0.6;
214 entry.access_count = 5;
215 entry.decay_score = 0.8;
216 let eff = DecayEngine::effective_importance(&entry);
217 assert!(
218 eff > 0.6,
219 "Effective importance should be boosted, got {}",
220 eff
221 );
222 }
223
224 #[test]
225 fn test_prunable() {
226 let engine = DecayEngine::new(1.0);
227 let mut entry = make_entry(0);
228 entry.decay_score = 0.01;
229 assert!(engine.is_prunable(&entry, 0.05));
230
231 entry.pinned = true;
232 assert!(!engine.is_prunable(&entry, 0.05));
233
234 entry.pinned = false;
235 entry.protection = ProtectionLevel::Medium;
236 assert!(!engine.is_prunable(&entry, 0.05));
237 }
238}