1use chrono::Utc;
7
8use crate::memory::types::{MemoryEntry, ProtectionLevel};
9
10#[derive(Debug, Clone)]
24pub struct AutoProtector {
25 pub protection_low_access: u32,
27 pub protection_medium_access: u32,
29 pub protection_high_access: u32,
31 pub protection_medium_sessions: u32,
33 pub protection_high_sessions: u32,
35 pub demotion_stale_days: u32,
37}
38
39impl AutoProtector {
40 pub fn new(
42 low_access: u32,
43 medium_access: u32,
44 high_access: u32,
45 medium_sessions: u32,
46 high_sessions: u32,
47 demotion_stale_days: u32,
48 ) -> Self {
49 Self {
50 protection_low_access: low_access,
51 protection_medium_access: medium_access,
52 protection_high_access: high_access,
53 protection_medium_sessions: medium_sessions,
54 protection_high_sessions: high_sessions,
55 demotion_stale_days,
56 }
57 }
58
59 pub fn default_protector() -> Self {
61 Self::new(2, 3, 5, 2, 3, 30)
62 }
63
64 pub fn compute_protection(&self, entry: &MemoryEntry) -> ProtectionLevel {
68 if entry.memory_type.is_auto_protected() {
70 return ProtectionLevel::Permanent;
71 }
72
73 if entry.pinned {
75 return ProtectionLevel::Permanent;
76 }
77
78 if entry.user_corrected {
80 return ProtectionLevel::High;
81 }
82
83 let access_count = entry.access_count;
85 let session_span = entry.session_appearances;
86
87 if access_count >= self.protection_high_access
89 || session_span >= self.protection_high_sessions
90 {
91 return ProtectionLevel::High;
92 }
93
94 if access_count >= self.protection_medium_access
96 || session_span >= self.protection_medium_sessions
97 {
98 return ProtectionLevel::Medium;
99 }
100
101 if access_count >= self.protection_low_access {
103 return ProtectionLevel::Low;
104 }
105
106 ProtectionLevel::None
108 }
109
110 pub fn should_demote_protection(
115 &self,
116 entry: &MemoryEntry,
117 current: ProtectionLevel,
118 ) -> Option<ProtectionLevel> {
119 if entry.pinned || current == ProtectionLevel::Permanent {
121 return None;
122 }
123
124 let days_since_access = (Utc::now() - entry.accessed_at).num_days() as u32;
125 let stale = self.demotion_stale_days;
126
127 if current == ProtectionLevel::High
129 && days_since_access > stale
130 && entry.access_count < self.protection_medium_access
131 {
132 return Some(ProtectionLevel::Medium);
133 }
134
135 if current == ProtectionLevel::Medium && days_since_access > stale * 2 {
137 return Some(ProtectionLevel::Low);
138 }
139
140 if current == ProtectionLevel::Low && days_since_access > stale * 3 {
142 return Some(ProtectionLevel::None);
143 }
144
145 None
146 }
147
148 pub fn record_access(entry: &mut MemoryEntry, current_session_id: &str) {
152 entry.access_count += 1;
153 entry.accessed_at = Utc::now();
154
155 if !entry
164 .seen_in_sessions
165 .contains(¤t_session_id.to_string())
166 {
167 entry.session_appearances += 1;
168 if entry.seen_in_sessions.len() < 100 {
169 entry.seen_in_sessions.push(current_session_id.to_string());
170 }
171 }
172
173 let boosted = 0.5 + 0.5 * entry.decay_score;
175 entry.decay_score = entry.decay_score.max(boosted);
176 }
177}
178
179#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::memory::{MemoryEntry, MemoryTier, MemoryType};
187 use chrono::Duration;
188
189 fn make_entry_with_access(access_count: u32, sessions: u32) -> MemoryEntry {
190 let mut entry = make_base_entry();
191 entry.access_count = access_count;
192 entry.session_appearances = sessions;
193 entry.seen_in_sessions = (0..sessions).map(|i| format!("session-{}", i)).collect();
194 entry
195 }
196
197 fn make_base_entry() -> MemoryEntry {
198 MemoryEntry {
199 id: "test".to_string(),
200 memory_type: MemoryType::Fact,
201 tier: MemoryTier::Warm,
202 content: "test".to_string(),
203 content_hash: 0,
204 tags: vec![],
205 source: "test".to_string(),
206 session_id: None,
207 importance: 0.5,
208 pinned: false,
209 protection: ProtectionLevel::None,
210 auto_classified: false,
211 session_appearances: 0,
212 user_corrected: false,
213 seen_in_sessions: vec![],
214 created_at: Utc::now(),
215 accessed_at: Utc::now(),
216 modified_at: Utc::now(),
217 access_count: 0,
218 decay_score: 1.0,
219 compaction_level: 0,
220 compacted_from: vec![],
221 related_ids: vec![],
222 contradicts: None,
223 }
224 }
225
226 #[test]
227 fn test_protection_none_default() {
228 let protector = AutoProtector::default_protector();
229 let entry = make_entry_with_access(0, 0);
230 assert_eq!(protector.compute_protection(&entry), ProtectionLevel::None);
231 }
232
233 #[test]
234 fn test_protection_low() {
235 let protector = AutoProtector::default_protector();
236 let entry = make_entry_with_access(2, 0);
237 assert_eq!(protector.compute_protection(&entry), ProtectionLevel::Low);
238 }
239
240 #[test]
241 fn test_protection_medium_access() {
242 let protector = AutoProtector::default_protector();
243 let entry = make_entry_with_access(3, 0);
244 assert_eq!(
245 protector.compute_protection(&entry),
246 ProtectionLevel::Medium
247 );
248 }
249
250 #[test]
251 fn test_protection_medium_sessions() {
252 let protector = AutoProtector::default_protector();
253 let entry = make_entry_with_access(0, 2);
254 assert_eq!(
255 protector.compute_protection(&entry),
256 ProtectionLevel::Medium
257 );
258 }
259
260 #[test]
261 fn test_protection_high_access() {
262 let protector = AutoProtector::default_protector();
263 let entry = make_entry_with_access(5, 0);
264 assert_eq!(protector.compute_protection(&entry), ProtectionLevel::High);
265 }
266
267 #[test]
268 fn test_protection_high_sessions() {
269 let protector = AutoProtector::default_protector();
270 let entry = make_entry_with_access(0, 3);
271 assert_eq!(protector.compute_protection(&entry), ProtectionLevel::High);
272 }
273
274 #[test]
275 fn test_protection_permanent_for_profile() {
276 let protector = AutoProtector::default_protector();
277 let mut entry = make_base_entry();
278 entry.memory_type = MemoryType::UserProfile;
279 assert_eq!(
280 protector.compute_protection(&entry),
281 ProtectionLevel::Permanent
282 );
283 }
284
285 #[test]
286 fn test_protection_permanent_for_preference() {
287 let protector = AutoProtector::default_protector();
288 let mut entry = make_base_entry();
289 entry.memory_type = MemoryType::Preference;
290 assert_eq!(
291 protector.compute_protection(&entry),
292 ProtectionLevel::Permanent
293 );
294 }
295
296 #[test]
297 fn test_protection_user_correction() {
298 let protector = AutoProtector::default_protector();
299 let mut entry = make_base_entry();
300 entry.user_corrected = true;
301 assert_eq!(protector.compute_protection(&entry), ProtectionLevel::High);
302 }
303
304 #[test]
305 fn test_protection_pinned() {
306 let protector = AutoProtector::default_protector();
307 let mut entry = make_base_entry();
308 entry.pinned = true;
309 assert_eq!(
310 protector.compute_protection(&entry),
311 ProtectionLevel::Permanent
312 );
313 }
314
315 #[test]
316 fn test_demote_high_to_medium() {
317 let protector = AutoProtector::default_protector();
318 let mut entry = make_entry_with_access(2, 0); entry.accessed_at = Utc::now() - Duration::days(35); let result = protector.should_demote_protection(&entry, ProtectionLevel::High);
321 assert_eq!(result, Some(ProtectionLevel::Medium));
322 }
323
324 #[test]
325 fn test_demote_medium_to_low() {
326 let protector = AutoProtector::default_protector();
327 let mut entry = make_entry_with_access(3, 1);
328 entry.accessed_at = Utc::now() - Duration::days(65); let result = protector.should_demote_protection(&entry, ProtectionLevel::Medium);
330 assert_eq!(result, Some(ProtectionLevel::Low));
331 }
332
333 #[test]
334 fn test_demote_low_to_none() {
335 let protector = AutoProtector::default_protector();
336 let mut entry = make_entry_with_access(2, 0);
337 entry.accessed_at = Utc::now() - Duration::days(95); let result = protector.should_demote_protection(&entry, ProtectionLevel::Low);
339 assert_eq!(result, Some(ProtectionLevel::None));
340 }
341
342 #[test]
343 fn test_no_demote_permanent() {
344 let protector = AutoProtector::default_protector();
345 let mut entry = make_base_entry();
346 entry.accessed_at = Utc::now() - Duration::days(365);
347 let result = protector.should_demote_protection(&entry, ProtectionLevel::Permanent);
348 assert_eq!(result, None);
349 }
350
351 #[test]
352 fn test_no_demote_pinned() {
353 let protector = AutoProtector::default_protector();
354 let mut entry = make_base_entry();
355 entry.pinned = true;
356 entry.accessed_at = Utc::now() - Duration::days(365);
357 let result = protector.should_demote_protection(&entry, ProtectionLevel::High);
358 assert_eq!(result, None);
359 }
360
361 #[test]
362 fn test_record_access() {
363 let mut entry = make_base_entry();
364 entry.decay_score = 0.2;
365 AutoProtector::record_access(&mut entry, "session-1");
366
367 assert_eq!(entry.access_count, 1);
368 assert_eq!(entry.session_appearances, 1);
369 assert!(entry.seen_in_sessions.contains(&"session-1".to_string()));
370 assert!(entry.decay_score > 0.2, "Should recover decay on access");
371 }
372
373 #[test]
374 fn test_record_access_dedup_session() {
375 let mut entry = make_base_entry();
376 AutoProtector::record_access(&mut entry, "session-1");
377 AutoProtector::record_access(&mut entry, "session-1");
378 assert_eq!(entry.access_count, 2);
379 assert_eq!(
380 entry.session_appearances, 1,
381 "Same session should not increment appearances"
382 );
383 }
384}