1use crate::constants::{
18 INTERFERENCE_COMPETITION_FACTOR, INTERFERENCE_MAX_TRACKED, INTERFERENCE_PROACTIVE_DECAY,
19 INTERFERENCE_PROACTIVE_THRESHOLD, INTERFERENCE_RETROACTIVE_DECAY,
20 INTERFERENCE_SEVERE_THRESHOLD, INTERFERENCE_SIMILARITY_THRESHOLD,
21 INTERFERENCE_VULNERABILITY_HOURS, REPLAY_AROUSAL_THRESHOLD, REPLAY_BATCH_SIZE,
22 REPLAY_EDGE_BOOST, REPLAY_IMPORTANCE_THRESHOLD, REPLAY_MAX_AGE_DAYS, REPLAY_STRENGTH_BOOST,
23};
24use crate::memory::introspection::{ConsolidationEvent, InterferenceType};
25use chrono::{DateTime, Duration, Utc};
26use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, HashSet};
28
29#[derive(Debug, Clone)]
31pub struct ReplayCandidate {
32 pub memory_id: String,
33 pub content_preview: String,
34 pub importance: f32,
35 pub arousal: f32,
36 pub age_days: f64,
37 pub connection_count: usize,
38 pub priority_score: f32,
39 pub connected_memory_ids: Vec<String>,
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct ReplayCycleResult {
45 pub memories_replayed: usize,
46 pub edges_strengthened: usize,
47 pub total_priority_score: f32,
48 pub events: Vec<ConsolidationEvent>,
49 pub edge_boosts: Vec<(String, String, f32)>,
52 pub replay_memory_ids: Vec<String>,
54}
55
56pub struct ReplayManager {
63 last_replay: DateTime<Utc>,
65 replay_interval_hours: i64,
67 total_replays: usize,
69}
70
71impl Default for ReplayManager {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl ReplayManager {
78 pub fn new() -> Self {
79 Self {
80 last_replay: Utc::now() - Duration::hours(24), replay_interval_hours: 1, total_replays: 0,
83 }
84 }
85
86 pub fn should_replay(&self) -> bool {
88 let elapsed = Utc::now() - self.last_replay;
89 elapsed.num_hours() >= self.replay_interval_hours
90 }
91
92 pub fn identify_replay_candidates(
100 &self,
101 memories: &[(String, f32, f32, DateTime<Utc>, Vec<String>, String)], ) -> Vec<ReplayCandidate> {
103 let now = Utc::now();
104 let mut candidates: Vec<ReplayCandidate> = memories
105 .iter()
106 .filter_map(
107 |(id, importance, arousal, created_at, connections, preview)| {
108 let age = now - *created_at;
109 let age_days = age.num_hours() as f64 / 24.0;
110
111 if age_days > REPLAY_MAX_AGE_DAYS as f64 {
113 return None;
114 }
115 if *importance < REPLAY_IMPORTANCE_THRESHOLD {
116 return None;
117 }
118 let recency_factor = 1.0 - (age_days / REPLAY_MAX_AGE_DAYS as f64) as f32;
124 let arousal_boost = if *arousal > REPLAY_AROUSAL_THRESHOLD {
125 (*arousal - REPLAY_AROUSAL_THRESHOLD) * 0.5
126 } else {
127 0.0
128 };
129 let connectivity_factor = 1.0 + (connections.len() as f32 / 10.0).min(0.5); let priority =
132 importance * recency_factor * (1.0 + arousal_boost) * connectivity_factor;
133
134 Some(ReplayCandidate {
135 memory_id: id.clone(),
136 content_preview: preview.clone(),
137 importance: *importance,
138 arousal: *arousal,
139 age_days,
140 connection_count: connections.len(),
141 priority_score: priority,
142 connected_memory_ids: connections.clone(),
143 })
144 },
145 )
146 .collect();
147
148 candidates.sort_by(|a, b| b.priority_score.total_cmp(&a.priority_score));
150
151 candidates.truncate(REPLAY_BATCH_SIZE);
153 candidates
154 }
155
156 pub fn execute_replay(
160 &mut self,
161 candidates: &[ReplayCandidate],
162 ) -> (
163 Vec<(String, f32)>,
164 Vec<(String, String, f32)>,
165 Vec<ConsolidationEvent>,
166 ) {
167 let mut memory_boosts: Vec<(String, f32)> = Vec::new();
169 let mut edge_boosts: Vec<(String, String, f32)> = Vec::new();
170 let mut events: Vec<ConsolidationEvent> = Vec::new();
171 let now = Utc::now();
172
173 let mut replayed: HashSet<String> = HashSet::new();
175
176 for candidate in candidates {
177 if replayed.contains(&candidate.memory_id) {
178 continue;
179 }
180
181 memory_boosts.push((candidate.memory_id.clone(), REPLAY_STRENGTH_BOOST));
183 replayed.insert(candidate.memory_id.clone());
184
185 let mut connected_replayed = 0;
187 for connected_id in &candidate.connected_memory_ids {
188 if !replayed.contains(connected_id) {
189 memory_boosts.push((connected_id.clone(), REPLAY_STRENGTH_BOOST * 0.5));
191 replayed.insert(connected_id.clone());
192 }
193
194 edge_boosts.push((
196 candidate.memory_id.clone(),
197 connected_id.clone(),
198 REPLAY_EDGE_BOOST,
199 ));
200 connected_replayed += 1;
201 }
202
203 events.push(ConsolidationEvent::MemoryReplayed {
205 memory_id: candidate.memory_id.clone(),
206 content_preview: candidate.content_preview.clone(),
207 activation_before: candidate.importance,
208 activation_after: (candidate.importance + REPLAY_STRENGTH_BOOST).min(1.0),
209 replay_priority: candidate.priority_score,
210 connected_memories_replayed: connected_replayed,
211 timestamp: now,
212 });
213 }
214
215 self.last_replay = now;
216 self.total_replays += replayed.len();
217
218 (memory_boosts, edge_boosts, events)
219 }
220
221 pub fn stats(&self) -> (usize, DateTime<Utc>) {
223 (self.total_replays, self.last_replay)
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct InterferenceRecord {
234 pub interfering_memory_id: String,
235 pub similarity: f32,
236 pub interference_type: InterferenceType,
237 pub strength_change: f32,
238 pub timestamp: DateTime<Utc>,
239}
240
241#[derive(Debug, Clone, Default)]
243pub struct InterferenceCheckResult {
244 pub retroactive_targets: Vec<(String, f32, f32)>, pub proactive_decay: f32,
248 pub is_duplicate: bool,
250 pub events: Vec<ConsolidationEvent>,
252}
253
254#[derive(Debug, Clone)]
256pub struct CompetitionResult {
257 pub winners: Vec<(String, f32)>, pub suppressed: Vec<String>,
261 pub competition_factor: f32,
263 pub event: Option<ConsolidationEvent>,
265}
266
267pub struct InterferenceDetector {
269 interference_history: HashMap<String, Vec<InterferenceRecord>>,
271 total_interference_events: usize,
273}
274
275impl Default for InterferenceDetector {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281impl InterferenceDetector {
282 pub fn new() -> Self {
283 Self {
284 interference_history: HashMap::new(),
285 total_interference_events: 0,
286 }
287 }
288
289 pub fn check_interference(
294 &mut self,
295 new_memory_id: &str,
296 new_memory_importance: f32,
297 _new_memory_created: DateTime<Utc>,
298 similar_memories: &[(String, f32, f32, DateTime<Utc>, String)], ) -> InterferenceCheckResult {
300 let mut result = InterferenceCheckResult::default();
301 let now = Utc::now();
302
303 for (old_id, similarity, old_importance, old_created, old_preview) in similar_memories {
304 if old_id == new_memory_id {
306 continue;
307 }
308
309 if *similarity < INTERFERENCE_SIMILARITY_THRESHOLD {
311 continue;
312 }
313
314 if *similarity >= INTERFERENCE_SEVERE_THRESHOLD {
316 result.is_duplicate = true;
317 return result;
319 }
320
321 let age_hours = (now - *old_created).num_hours();
323 let is_vulnerable = age_hours < INTERFERENCE_VULNERABILITY_HOURS;
324
325 if is_vulnerable || *old_importance < new_memory_importance {
327 let interference_strength = (*similarity - INTERFERENCE_SIMILARITY_THRESHOLD)
329 / (1.0 - INTERFERENCE_SIMILARITY_THRESHOLD);
330
331 let decay = INTERFERENCE_RETROACTIVE_DECAY * interference_strength;
332 result
333 .retroactive_targets
334 .push((old_id.clone(), *similarity, decay));
335
336 result
338 .events
339 .push(ConsolidationEvent::InterferenceDetected {
340 new_memory_id: new_memory_id.to_string(),
341 old_memory_id: old_id.clone(),
342 similarity: *similarity,
343 interference_type: InterferenceType::Retroactive,
344 timestamp: now,
345 });
346
347 result.events.push(ConsolidationEvent::MemoryWeakened {
348 memory_id: old_id.clone(),
349 content_preview: old_preview.clone(),
350 activation_before: *old_importance,
351 activation_after: (*old_importance - decay).max(0.05),
352 interfering_memory_id: new_memory_id.to_string(),
353 interference_type: InterferenceType::Retroactive,
354 timestamp: now,
355 });
356
357 self.record_interference(
359 old_id,
360 new_memory_id,
361 *similarity,
362 InterferenceType::Retroactive,
363 decay,
364 );
365 }
366
367 if *old_importance > INTERFERENCE_PROACTIVE_THRESHOLD {
369 let interference_strength = (*similarity - INTERFERENCE_SIMILARITY_THRESHOLD)
370 / (1.0 - INTERFERENCE_SIMILARITY_THRESHOLD);
371
372 let decay = INTERFERENCE_PROACTIVE_DECAY
373 * interference_strength
374 * (*old_importance - INTERFERENCE_PROACTIVE_THRESHOLD);
375
376 result.proactive_decay += decay;
377
378 result
379 .events
380 .push(ConsolidationEvent::InterferenceDetected {
381 new_memory_id: new_memory_id.to_string(),
382 old_memory_id: old_id.clone(),
383 similarity: *similarity,
384 interference_type: InterferenceType::Proactive,
385 timestamp: now,
386 });
387
388 self.record_interference(
389 new_memory_id,
390 old_id,
391 *similarity,
392 InterferenceType::Proactive,
393 decay,
394 );
395 }
396 }
397
398 self.total_interference_events += result.events.len();
399 result
400 }
401
402 pub fn apply_retrieval_competition(
407 &mut self,
408 candidates: &[(String, f32, f32)], query_preview: &str,
410 ) -> CompetitionResult {
411 if candidates.len() <= 1 {
412 return CompetitionResult {
413 winners: candidates
414 .iter()
415 .map(|(id, score, _)| (id.clone(), *score))
416 .collect(),
417 suppressed: Vec::new(),
418 competition_factor: 0.0,
419 event: None,
420 };
421 }
422
423 let mut scores: Vec<(String, f32)> = candidates
425 .iter()
426 .map(|(id, score, _)| (id.clone(), *score))
427 .collect();
428
429 scores.sort_by(|a, b| b.1.total_cmp(&a.1));
431
432 let mut winners: Vec<(String, f32)> = Vec::new();
433 let mut suppressed: Vec<String> = Vec::new();
434
435 if let Some((winner_id, winner_score)) = scores.first() {
436 winners.push((winner_id.clone(), *winner_score));
437
438 if *winner_score <= 0.0 {
440 for (id, score) in scores.iter().skip(1) {
441 winners.push((id.clone(), *score));
442 }
443 } else {
444 for (id, score) in scores.iter().skip(1) {
448 let score_ratio = score / winner_score;
449
450 if score_ratio > 0.9 {
452 let suppression =
453 INTERFERENCE_COMPETITION_FACTOR * (1.0 - score_ratio) * 10.0;
454 let new_score = (score - suppression).max(0.0);
455
456 if new_score > 0.1 {
457 winners.push((id.clone(), new_score));
458 self.record_interference(
460 id,
461 winner_id,
462 score_ratio,
463 InterferenceType::RetrievalCompetition,
464 suppression * 0.3,
465 );
466 } else {
467 suppressed.push(id.clone());
468 self.record_interference(
470 id,
471 winner_id,
472 score_ratio,
473 InterferenceType::RetrievalCompetition,
474 suppression,
475 );
476 }
477 } else {
478 winners.push((id.clone(), *score));
479 }
480 }
481
482 self.total_interference_events += suppressed.len();
483 }
484 }
485
486 let event = if !suppressed.is_empty() {
487 Some(ConsolidationEvent::RetrievalCompetition {
488 query_preview: query_preview.to_string(),
489 winner_memory_id: winners
490 .first()
491 .map(|(id, _)| id.clone())
492 .unwrap_or_default(),
493 suppressed_memory_ids: suppressed.clone(),
494 competition_factor: INTERFERENCE_COMPETITION_FACTOR,
495 timestamp: Utc::now(),
496 })
497 } else {
498 None
499 };
500
501 CompetitionResult {
502 winners,
503 suppressed,
504 competition_factor: INTERFERENCE_COMPETITION_FACTOR,
505 event,
506 }
507 }
508
509 pub(crate) fn record_interference(
511 &mut self,
512 affected_memory_id: &str,
513 interfering_memory_id: &str,
514 similarity: f32,
515 interference_type: InterferenceType,
516 strength_change: f32,
517 ) {
518 let record = InterferenceRecord {
519 interfering_memory_id: interfering_memory_id.to_string(),
520 similarity,
521 interference_type,
522 strength_change,
523 timestamp: Utc::now(),
524 };
525
526 let history = self
527 .interference_history
528 .entry(affected_memory_id.to_string())
529 .or_default();
530
531 history.push(record);
532
533 if history.len() > INTERFERENCE_MAX_TRACKED {
535 history.remove(0);
536 }
537 }
538
539 pub fn get_history(&self, memory_id: &str) -> Option<&Vec<InterferenceRecord>> {
541 self.interference_history.get(memory_id)
542 }
543
544 pub fn stats(&self) -> (usize, usize) {
546 (
547 self.total_interference_events,
548 self.interference_history.len(),
549 )
550 }
551
552 pub fn clear_memory(&mut self, memory_id: &str) {
554 self.interference_history.remove(memory_id);
555 }
556
557 pub fn calculate_retrieval_adjustment(&self, memory_id: &str, current_activation: f32) -> f32 {
595 let history = match self.interference_history.get(memory_id) {
596 Some(h) if !h.is_empty() => h,
597 _ => return 1.0, };
599
600 let interference_count = history.len();
602 let total_strength_lost: f32 = history.iter().map(|r| r.strength_change.abs()).sum();
603 let avg_similarity: f32 =
604 history.iter().map(|r| r.similarity).sum::<f32>() / interference_count as f32;
605
606 let count_factor = (interference_count as f32 / INTERFERENCE_MAX_TRACKED as f32).min(1.0);
610 let damage_factor = (total_strength_lost / 0.5).min(1.0); let interference_intensity = (count_factor * 0.5 + damage_factor * 0.5) * avg_similarity;
612
613 let activation_factor = 2.0 * current_activation - 1.0; let adjustment = 1.0 + interference_intensity * activation_factor * 0.5;
624
625 adjustment.clamp(0.5, 1.5)
627 }
628
629 pub fn batch_retrieval_adjustments(&self, memories: &[(String, f32)]) -> HashMap<String, f32> {
639 memories
640 .iter()
641 .map(|(id, activation)| {
642 (
643 id.clone(),
644 self.calculate_retrieval_adjustment(id, *activation),
645 )
646 })
647 .collect()
648 }
649
650 pub fn has_significant_interference(&self, memory_id: &str) -> bool {
654 self.interference_history
655 .get(memory_id)
656 .map(|h| h.len() >= 2) .unwrap_or(false)
658 }
659
660 pub fn load_history(
669 &mut self,
670 history: HashMap<String, Vec<InterferenceRecord>>,
671 total_events: usize,
672 ) {
673 self.interference_history = history;
674 self.total_interference_events = total_events;
675 tracing::info!(
676 memories_tracked = self.interference_history.len(),
677 total_events = self.total_interference_events,
678 "Loaded interference history from persistent storage"
679 );
680 }
681
682 pub fn get_affected_ids_from_check(
686 &self,
687 new_memory_id: &str,
688 result: &InterferenceCheckResult,
689 ) -> Vec<String> {
690 let mut ids: Vec<String> = result
691 .retroactive_targets
692 .iter()
693 .map(|(id, _, _)| id.clone())
694 .collect();
695
696 if result.proactive_decay > 0.0 {
697 ids.push(new_memory_id.to_string());
698 }
699
700 ids
701 }
702
703 pub fn get_affected_ids_from_competition(&self, result: &CompetitionResult) -> Vec<String> {
708 let mut ids = result.suppressed.clone();
709
710 if let Some((_, winner_score)) = result.winners.first() {
712 if *winner_score > 0.0 {
713 for (id, score) in result.winners.iter().skip(1) {
714 let score_ratio = score / winner_score;
715 if score_ratio > 0.9 {
716 ids.push(id.clone());
717 }
718 }
719 }
720 }
721
722 ids
723 }
724
725 pub fn get_records_for_ids<'a>(
727 &'a self,
728 ids: &'a [String],
729 ) -> Vec<(&'a str, &'a Vec<InterferenceRecord>)> {
730 ids.iter()
731 .filter_map(|id| {
732 self.interference_history
733 .get(id)
734 .map(|records| (id.as_str(), records))
735 })
736 .collect()
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_replay_candidate_identification() {
746 let manager = ReplayManager::new();
747 let now = Utc::now();
748
749 let memories = vec![
751 (
752 "mem-1".to_string(),
753 0.8, 0.7, now - Duration::hours(12), vec!["mem-2".to_string(), "mem-3".to_string()], "Important memory".to_string(),
758 ),
759 (
760 "mem-2".to_string(),
761 0.2, 0.3,
763 now - Duration::hours(6),
764 vec!["mem-1".to_string()],
765 "Unimportant memory".to_string(),
766 ),
767 (
768 "mem-3".to_string(),
769 0.6,
770 0.4,
771 now - Duration::days(15), vec!["mem-1".to_string(), "mem-4".to_string()],
773 "Old memory".to_string(),
774 ),
775 ];
776
777 let candidates = manager.identify_replay_candidates(&memories);
778
779 assert_eq!(candidates.len(), 1);
781 assert_eq!(candidates[0].memory_id, "mem-1");
782 assert!(candidates[0].priority_score > 0.0);
783 }
784
785 #[test]
786 fn test_replay_execution() {
787 let mut manager = ReplayManager::new();
788 let _now = Utc::now();
789
790 let candidates = vec![ReplayCandidate {
791 memory_id: "mem-1".to_string(),
792 content_preview: "Test memory".to_string(),
793 importance: 0.7,
794 arousal: 0.6,
795 age_days: 1.0,
796 connection_count: 2,
797 priority_score: 0.8,
798 connected_memory_ids: vec!["mem-2".to_string(), "mem-3".to_string()],
799 }];
800
801 let (memory_boosts, edge_boosts, events) = manager.execute_replay(&candidates);
802
803 assert!(memory_boosts.iter().any(|(id, _)| id == "mem-1"));
805
806 assert!(memory_boosts.iter().any(|(id, _)| id == "mem-2"));
808 assert!(memory_boosts.iter().any(|(id, _)| id == "mem-3"));
809
810 assert_eq!(edge_boosts.len(), 2);
812
813 assert_eq!(events.len(), 1);
815 }
816
817 #[test]
818 fn test_interference_detection() {
819 let mut detector = InterferenceDetector::new();
820 let now = Utc::now();
821
822 let similar_memories = vec![(
824 "old-mem".to_string(),
825 0.90, 0.5, now - Duration::hours(12), "Old memory content".to_string(),
829 )];
830
831 let result = detector.check_interference(
832 "new-mem",
833 0.7, now,
835 &similar_memories,
836 );
837
838 assert!(!result.retroactive_targets.is_empty());
840 assert!(!result.events.is_empty());
841 }
842
843 #[test]
844 fn test_duplicate_detection() {
845 let mut detector = InterferenceDetector::new();
846 let now = Utc::now();
847
848 let similar_memories = vec![(
850 "existing-mem".to_string(),
851 0.98, 0.5,
853 now - Duration::hours(1),
854 "Existing content".to_string(),
855 )];
856
857 let result = detector.check_interference("new-mem", 0.6, now, &similar_memories);
858
859 assert!(result.is_duplicate);
861 assert!(result.events.is_empty());
863 }
864
865 #[test]
866 fn test_retrieval_competition() {
867 let mut detector = InterferenceDetector::new();
868
869 let candidates = vec![
870 ("mem-1".to_string(), 0.9, 0.85), ("mem-2".to_string(), 0.88, 0.82), ("mem-3".to_string(), 0.5, 0.70), ];
874
875 let result = detector.apply_retrieval_competition(&candidates, "test query");
876
877 assert_eq!(result.winners[0].0, "mem-1");
879 assert!(!result.winners.is_empty());
881 }
882
883 #[test]
888 fn test_retrieval_adjustment_no_history() {
889 let detector = InterferenceDetector::new();
890
891 let adjustment = detector.calculate_retrieval_adjustment("unknown-mem", 0.8);
893 assert_eq!(
894 adjustment, 1.0,
895 "No history should return neutral adjustment"
896 );
897 }
898
899 #[test]
900 fn test_retrieval_adjustment_survivor_boost() {
901 let mut detector = InterferenceDetector::new();
902 let now = Utc::now();
903
904 let _similar_memories = vec![(
907 "survivor-mem".to_string(),
908 0.90,
909 0.85, now - Duration::hours(12),
911 "Survivor content".to_string(),
912 )];
913
914 for i in 0..5 {
916 detector.record_interference(
917 "survivor-mem",
918 &format!("interferer-{}", i),
919 0.88,
920 InterferenceType::Retroactive,
921 0.05,
922 );
923 }
924
925 let adjustment = detector.calculate_retrieval_adjustment("survivor-mem", 0.9);
927 assert!(
928 adjustment > 1.0,
929 "Survivor (high activation despite interference) should be boosted: {}",
930 adjustment
931 );
932 }
933
934 #[test]
935 fn test_retrieval_adjustment_chronic_loser_suppress() {
936 let mut detector = InterferenceDetector::new();
937
938 for i in 0..5 {
941 detector.record_interference(
942 "loser-mem",
943 &format!("winner-{}", i),
944 0.88,
945 InterferenceType::Retroactive,
946 0.1,
947 );
948 }
949
950 let adjustment = detector.calculate_retrieval_adjustment("loser-mem", 0.2);
952 assert!(
953 adjustment < 1.0,
954 "Chronic loser (low activation with interference) should be suppressed: {}",
955 adjustment
956 );
957 }
958
959 #[test]
960 fn test_retrieval_adjustment_neutral_midpoint() {
961 let mut detector = InterferenceDetector::new();
962
963 for i in 0..3 {
965 detector.record_interference(
966 "neutral-mem",
967 &format!("other-{}", i),
968 0.87,
969 InterferenceType::Retroactive,
970 0.05,
971 );
972 }
973
974 let adjustment = detector.calculate_retrieval_adjustment("neutral-mem", 0.5);
976 assert!(
977 (adjustment - 1.0).abs() < 0.1,
978 "Medium activation should be near neutral: {}",
979 adjustment
980 );
981 }
982
983 #[test]
984 fn test_batch_retrieval_adjustments() {
985 let mut detector = InterferenceDetector::new();
986
987 for i in 0..3 {
989 detector.record_interference(
990 "mem-with-history",
991 &format!("interferer-{}", i),
992 0.88,
993 InterferenceType::Retroactive,
994 0.05,
995 );
996 }
997
998 let memories = vec![
999 ("mem-with-history".to_string(), 0.9), ("mem-no-history".to_string(), 0.9), ];
1002
1003 let adjustments = detector.batch_retrieval_adjustments(&memories);
1004
1005 assert!(adjustments.get("mem-with-history").unwrap() > &1.0);
1007 assert_eq!(adjustments.get("mem-no-history").unwrap(), &1.0);
1009 }
1010}