1use super::traits::{MemoryCategory, MemoryEntry};
2use chrono::{DateTime, Utc};
3
4pub const DEFAULT_HALF_LIFE_DAYS: f64 = 7.0;
7
8pub fn apply_time_decay(entries: &mut [MemoryEntry], half_life_days: f64) {
16 let half_life = if half_life_days <= 0.0 {
17 DEFAULT_HALF_LIFE_DAYS
18 } else {
19 half_life_days
20 };
21
22 let now = Utc::now();
23
24 for entry in entries.iter_mut() {
25 if entry.category == MemoryCategory::Core {
27 continue;
28 }
29
30 let score = match entry.score {
31 Some(s) => s,
32 None => continue,
33 };
34
35 let ts = match DateTime::parse_from_rfc3339(&entry.timestamp) {
36 Ok(dt) => dt.with_timezone(&Utc),
37 Err(_) => continue,
38 };
39
40 let age_days = now.signed_duration_since(ts).num_seconds().max(0) as f64 / 86_400.0;
41
42 let decay_factor = (-age_days / half_life * std::f64::consts::LN_2).exp();
43 entry.score = Some(score * decay_factor);
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 fn make_entry(category: MemoryCategory, score: Option<f64>, timestamp: &str) -> MemoryEntry {
52 MemoryEntry {
53 id: "1".into(),
54 key: "test".into(),
55 content: "value".into(),
56 category,
57 timestamp: timestamp.into(),
58 session_id: None,
59 score,
60 namespace: "default".into(),
61 importance: None,
62 superseded_by: None,
63 agent_alias: None,
64 agent_id: None,
65 }
66 }
67
68 fn recent_rfc3339() -> String {
69 Utc::now().to_rfc3339()
70 }
71
72 fn days_ago_rfc3339(days: i64) -> String {
73 (Utc::now() - chrono::Duration::days(days)).to_rfc3339()
74 }
75
76 #[test]
77 fn core_memories_are_never_decayed() {
78 let mut entries = vec![make_entry(
79 MemoryCategory::Core,
80 Some(0.9),
81 &days_ago_rfc3339(30),
82 )];
83 apply_time_decay(&mut entries, 7.0);
84 assert_eq!(entries[0].score, Some(0.9));
85 }
86
87 #[test]
88 fn recent_entry_score_barely_changes() {
89 let mut entries = vec![make_entry(
90 MemoryCategory::Conversation,
91 Some(0.8),
92 &recent_rfc3339(),
93 )];
94 apply_time_decay(&mut entries, 7.0);
95 let decayed = entries[0].score.unwrap();
96 assert!(
97 (decayed - 0.8).abs() < 0.01,
98 "recent entry should barely decay, got {decayed}"
99 );
100 }
101
102 #[test]
103 fn one_half_life_halves_score() {
104 let mut entries = vec![make_entry(
105 MemoryCategory::Conversation,
106 Some(1.0),
107 &days_ago_rfc3339(7),
108 )];
109 apply_time_decay(&mut entries, 7.0);
110 let decayed = entries[0].score.unwrap();
111 assert!(
112 (decayed - 0.5).abs() < 0.05,
113 "score after one half-life should be ~0.5, got {decayed}"
114 );
115 }
116
117 #[test]
118 fn two_half_lives_quarters_score() {
119 let mut entries = vec![make_entry(
120 MemoryCategory::Conversation,
121 Some(1.0),
122 &days_ago_rfc3339(14),
123 )];
124 apply_time_decay(&mut entries, 7.0);
125 let decayed = entries[0].score.unwrap();
126 assert!(
127 (decayed - 0.25).abs() < 0.05,
128 "score after two half-lives should be ~0.25, got {decayed}"
129 );
130 }
131
132 #[test]
133 fn no_score_entry_is_unchanged() {
134 let mut entries = vec![make_entry(
135 MemoryCategory::Conversation,
136 None,
137 &days_ago_rfc3339(30),
138 )];
139 apply_time_decay(&mut entries, 7.0);
140 assert_eq!(entries[0].score, None);
141 }
142
143 #[test]
144 fn unparseable_timestamp_is_unchanged() {
145 let mut entries = vec![make_entry(
146 MemoryCategory::Conversation,
147 Some(0.9),
148 "not-a-date",
149 )];
150 apply_time_decay(&mut entries, 7.0);
151 assert_eq!(entries[0].score, Some(0.9));
152 }
153}