construct/memory/
decay.rs1use super::traits::{MemoryCategory, MemoryEntry};
7use chrono::{DateTime, Utc};
8
9pub const DEFAULT_HALF_LIFE_DAYS: f64 = 7.0;
12
13pub fn apply_time_decay(entries: &mut [MemoryEntry], half_life_days: f64) {
21 let half_life = if half_life_days <= 0.0 {
22 DEFAULT_HALF_LIFE_DAYS
23 } else {
24 half_life_days
25 };
26
27 let now = Utc::now();
28
29 for entry in entries.iter_mut() {
30 if entry.category == MemoryCategory::Core {
32 continue;
33 }
34
35 let score = match entry.score {
36 Some(s) => s,
37 None => continue,
38 };
39
40 let ts = match DateTime::parse_from_rfc3339(&entry.timestamp) {
41 Ok(dt) => dt.with_timezone(&Utc),
42 Err(_) => continue,
43 };
44
45 let age_days = now.signed_duration_since(ts).num_seconds().max(0) as f64 / 86_400.0;
46
47 let decay_factor = (-age_days / half_life * std::f64::consts::LN_2).exp();
48 entry.score = Some(score * decay_factor);
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 fn make_entry(category: MemoryCategory, score: Option<f64>, timestamp: &str) -> MemoryEntry {
57 MemoryEntry {
58 id: "1".into(),
59 key: "test".into(),
60 content: "value".into(),
61 category,
62 timestamp: timestamp.into(),
63 session_id: None,
64 score,
65 namespace: "default".into(),
66 importance: None,
67 superseded_by: None,
68 }
69 }
70
71 fn days_ago_rfc3339(days: i64) -> String {
72 (Utc::now() - chrono::Duration::days(days)).to_rfc3339()
73 }
74
75 #[test]
76 fn core_memories_are_never_decayed() {
77 let mut entries = vec![make_entry(
78 MemoryCategory::Core,
79 Some(0.9),
80 &days_ago_rfc3339(30),
81 )];
82 apply_time_decay(&mut entries, 7.0);
83 assert_eq!(entries[0].score, Some(0.9));
84 }
85
86 #[test]
87 fn one_half_life_halves_score() {
88 let mut entries = vec![make_entry(
89 MemoryCategory::Conversation,
90 Some(1.0),
91 &days_ago_rfc3339(7),
92 )];
93 apply_time_decay(&mut entries, 7.0);
94 let decayed = entries[0].score.unwrap();
95 assert!(
96 (decayed - 0.5).abs() < 0.05,
97 "score after one half-life should be ~0.5, got {decayed}"
98 );
99 }
100
101 #[test]
102 fn no_score_entry_is_unchanged() {
103 let mut entries = vec![make_entry(
104 MemoryCategory::Conversation,
105 None,
106 &days_ago_rfc3339(30),
107 )];
108 apply_time_decay(&mut entries, 7.0);
109 assert_eq!(entries[0].score, None);
110 }
111}