codex_memory/memory/
cognitive_consolidation.rs

1//! Cognitive Science-Based Memory Consolidation Implementation
2//!
3//! This module implements memory consolidation mechanics based on established
4//! cognitive science research, particularly focusing on the spacing effect,
5//! strength-dependent forgetting, and semantic clustering principles.
6//!
7//! ## Research Foundation
8//!
9//! ### Core Principles
10//! 1. **Ebbinghaus Forgetting Curve (1885)**: Exponential decay of memory strength
11//! 2. **Spacing Effect (Cepeda et al., 2006)**: Distributed practice enhances retention
12//! 3. **Testing Effect (Roediger & Karpicke, 2006)**: Retrieval practice strengthens memories
13//! 4. **Strength-Dependent Forgetting (Wickelgren, 1974)**: Strong memories decay more slowly
14//! 5. **Semantic Network Theory (Collins & Loftus, 1975)**: Memories form interconnected networks
15//!
16//! ## Mathematical Models
17//!
18//! ### Enhanced Recall Probability
19//! ```text
20//! P(recall) = r × exp(-g × t / (1 + n)) × cos_similarity × context_boost
21//! ```
22//! Where:
23//! - r = base recall strength (decay rate adaptation)
24//! - g = consolidation strength (strengthened by successful retrievals)
25//! - t = time since last access (normalized)
26//! - n = access count (implements testing effect)
27//! - cos_similarity = semantic relatedness to current context
28//! - context_boost = environmental/emotional context matching
29//!
30//! ### Consolidation Strength Update
31//! ```text
32//! gn = gn-1 + α × (1 - e^(-βt)) / (1 + e^(-βt)) × difficulty_factor
33//! ```
34//! Where:
35//! - α = learning rate (based on individual differences)
36//! - β = spacing sensitivity parameter
37//! - difficulty_factor = retrieval effort (desirable difficulty principle)
38
39use super::error::{MemoryError, Result};
40use super::math_engine::{MathEngine, MemoryParameters};
41use super::models::*;
42use chrono::Utc;
43use pgvector::Vector;
44use serde::{Deserialize, Serialize};
45use sqlx::postgres::types::PgInterval;
46use std::collections::HashMap;
47use tracing::{info, warn};
48use uuid::Uuid;
49
50/// Enhanced consolidation parameters incorporating cognitive research
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CognitiveConsolidationConfig {
53    /// Learning rate factor (individual differences)
54    pub alpha: f64,
55
56    /// Spacing sensitivity parameter
57    pub beta: f64,
58
59    /// Context similarity weight
60    pub context_weight: f64,
61
62    /// Semantic clustering threshold
63    pub clustering_threshold: f64,
64
65    /// Minimum consolidation interval (spacing effect)
66    pub min_spacing_hours: f64,
67
68    /// Maximum consolidation strength
69    pub max_strength: f64,
70
71    /// Difficulty scaling factor
72    pub difficulty_scaling: f64,
73}
74
75impl Default for CognitiveConsolidationConfig {
76    fn default() -> Self {
77        Self {
78            alpha: 0.3,                 // Conservative learning rate
79            beta: 1.5,                  // Moderate spacing sensitivity
80            context_weight: 0.2,        // Moderate context influence
81            clustering_threshold: 0.75, // High similarity for clustering
82            min_spacing_hours: 0.5,     // 30 minutes minimum spacing
83            max_strength: 15.0,         // Higher ceiling for expertise
84            difficulty_scaling: 1.2,    // Slight boost for difficult retrievals
85        }
86    }
87}
88
89/// Retrieval context for consolidation calculations
90#[derive(Debug, Clone)]
91pub struct RetrievalContext {
92    pub query_embedding: Option<Vector>,
93    pub environmental_factors: HashMap<String, f64>,
94    pub retrieval_latency_ms: u64,
95    pub confidence_score: f64,
96    pub related_memories: Vec<Uuid>,
97}
98
99/// Enhanced consolidation result with cognitive metrics
100#[derive(Debug, Clone)]
101pub struct CognitiveConsolidationResult {
102    pub new_consolidation_strength: f64,
103    pub strength_increment: f64,
104    pub recall_probability: f64,
105    pub spacing_bonus: f64,
106    pub difficulty_bonus: f64,
107    pub context_similarity: f64,
108    pub calculation_time_ms: u64,
109    pub cognitive_factors: CognitiveFactors,
110}
111
112/// Cognitive factors influencing consolidation
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct CognitiveFactors {
115    pub spacing_effect_strength: f64,
116    pub testing_effect_strength: f64,
117    pub semantic_clustering_bonus: f64,
118    pub context_dependent_boost: f64,
119    pub interference_penalty: f64,
120}
121
122/// Cognitive consolidation engine implementing research-backed algorithms
123pub struct CognitiveConsolidationEngine {
124    config: CognitiveConsolidationConfig,
125    math_engine: MathEngine,
126}
127
128impl CognitiveConsolidationEngine {
129    pub fn new(config: CognitiveConsolidationConfig) -> Self {
130        Self {
131            config,
132            math_engine: MathEngine::new(),
133        }
134    }
135
136    /// Calculate enhanced consolidation with cognitive factors
137    ///
138    /// This method implements the complete cognitive model including:
139    /// - Spacing effect calculations
140    /// - Testing effect strength
141    /// - Semantic similarity bonuses
142    /// - Context-dependent memory effects
143    /// - Interference calculations
144    pub async fn calculate_cognitive_consolidation(
145        &self,
146        memory: &Memory,
147        context: &RetrievalContext,
148        similar_memories: &[Memory],
149    ) -> Result<CognitiveConsolidationResult> {
150        let start_time = std::time::Instant::now();
151
152        // Calculate base consolidation using existing math engine
153        let params = MemoryParameters {
154            consolidation_strength: memory.consolidation_strength,
155            decay_rate: memory.decay_rate,
156            last_accessed_at: memory.last_accessed_at,
157            created_at: memory.created_at,
158            access_count: memory.access_count,
159            importance_score: memory.importance_score,
160        };
161
162        let _base_recall = self.math_engine.calculate_recall_probability(&params)?;
163
164        // Calculate spacing effect strength
165        let spacing_effect = self.calculate_spacing_effect(memory)?;
166
167        // Calculate testing effect based on retrieval difficulty
168        let testing_effect = self.calculate_testing_effect(context)?;
169
170        // Calculate semantic clustering bonus
171        let clustering_bonus =
172            self.calculate_semantic_clustering_bonus(memory, similar_memories)?;
173
174        // Calculate context-dependent memory boost
175        let context_boost = self.calculate_context_boost(memory, context)?;
176
177        // Calculate interference from similar memories
178        let interference_penalty = self.calculate_interference_penalty(memory, similar_memories)?;
179
180        // Combine all factors for enhanced consolidation strength
181        let strength_increment = self.calculate_enhanced_strength_increment(
182            memory,
183            spacing_effect,
184            testing_effect,
185            clustering_bonus,
186            context_boost,
187            interference_penalty,
188        )?;
189
190        let new_strength = (memory.consolidation_strength + strength_increment)
191            .min(self.config.max_strength)
192            .max(0.1);
193
194        // Recalculate recall probability with new strength
195        let enhanced_params = MemoryParameters {
196            consolidation_strength: new_strength,
197            ..params
198        };
199
200        let enhanced_recall = self
201            .math_engine
202            .calculate_recall_probability(&enhanced_params)?;
203
204        let calculation_time = start_time.elapsed().as_millis() as u64;
205
206        Ok(CognitiveConsolidationResult {
207            new_consolidation_strength: new_strength,
208            strength_increment,
209            recall_probability: enhanced_recall.recall_probability,
210            spacing_bonus: spacing_effect,
211            difficulty_bonus: testing_effect,
212            context_similarity: context_boost,
213            calculation_time_ms: calculation_time,
214            cognitive_factors: CognitiveFactors {
215                spacing_effect_strength: spacing_effect,
216                testing_effect_strength: testing_effect,
217                semantic_clustering_bonus: clustering_bonus,
218                context_dependent_boost: context_boost,
219                interference_penalty,
220            },
221        })
222    }
223
224    /// Calculate spacing effect strength based on retrieval intervals
225    ///
226    /// Implements findings from Cepeda et al. (2006) that optimal spacing
227    /// intervals depend on retention interval and create non-linear benefits.
228    fn calculate_spacing_effect(&self, memory: &Memory) -> Result<f64> {
229        let current_time = Utc::now();
230
231        // Calculate time since last access
232        let last_access = memory.last_accessed_at.unwrap_or(memory.created_at);
233        let interval_hours = current_time
234            .signed_duration_since(last_access)
235            .num_seconds() as f64
236            / 3600.0;
237
238        // Spacing effect follows inverted-U curve: too short = poor, too long = forgotten
239        if interval_hours < self.config.min_spacing_hours {
240            // Too recent - minimal spacing benefit
241            return Ok(0.1);
242        }
243
244        // Optimal spacing based on current consolidation strength
245        // Stronger memories benefit from longer intervals
246        let optimal_interval = memory.consolidation_strength * 24.0; // hours
247
248        // Calculate spacing effect using research-based curve
249        let spacing_ratio = interval_hours / optimal_interval;
250        let spacing_effect = if spacing_ratio < 0.5 {
251            // Sub-optimal: too short
252            spacing_ratio * 2.0
253        } else if spacing_ratio <= 2.0 {
254            // Optimal range: strong spacing effect
255            1.0 + (spacing_ratio - 1.0) * 0.5
256        } else {
257            // Too long: diminishing returns
258            1.5 * (2.0 / spacing_ratio).min(1.0)
259        };
260
261        Ok(spacing_effect.max(0.1).min(2.0))
262    }
263
264    /// Calculate testing effect based on retrieval difficulty
265    ///
266    /// Implements findings from Bjork (1994) that desirable difficulties
267    /// during retrieval enhance long-term retention.
268    fn calculate_testing_effect(&self, context: &RetrievalContext) -> Result<f64> {
269        // Convert retrieval latency to difficulty score
270        let difficulty = match context.retrieval_latency_ms {
271            0..=500 => 0.2,     // Too easy - minimal benefit
272            501..=2000 => 1.0,  // Optimal difficulty
273            2001..=5000 => 1.5, // High difficulty - strong benefit
274            _ => 0.8,           // Too difficult - reduced benefit
275        };
276
277        // Adjust by confidence - lower confidence indicates more effort
278        let confidence_factor = 1.0 + (1.0 - context.confidence_score) * 0.5;
279
280        let testing_effect = difficulty * confidence_factor * self.config.difficulty_scaling;
281
282        Ok(testing_effect.max(0.1).min(2.0))
283    }
284
285    /// Calculate semantic clustering bonus
286    ///
287    /// Implements semantic network theory (Collins & Loftus, 1975) where
288    /// memories in dense semantic clusters are more accessible.
289    fn calculate_semantic_clustering_bonus(
290        &self,
291        memory: &Memory,
292        similar_memories: &[Memory],
293    ) -> Result<f64> {
294        if similar_memories.is_empty() || memory.embedding.is_none() {
295            return Ok(0.0);
296        }
297
298        let memory_embedding = memory.embedding.as_ref().unwrap();
299        let mut similarity_sum = 0.0;
300        let mut high_similarity_count = 0;
301
302        for similar_memory in similar_memories {
303            if let Some(similar_embedding) = &similar_memory.embedding {
304                // Calculate cosine similarity
305                let similarity =
306                    self.calculate_cosine_similarity(memory_embedding, similar_embedding)?;
307
308                if similarity > self.config.clustering_threshold {
309                    high_similarity_count += 1;
310                    similarity_sum += similarity;
311                }
312            }
313        }
314
315        if high_similarity_count == 0 {
316            return Ok(0.0);
317        }
318
319        // Clustering bonus based on density of highly similar memories
320        let avg_similarity = similarity_sum / high_similarity_count as f64;
321        let density_bonus = (high_similarity_count as f64).ln() / 10.0; // Logarithmic scaling
322
323        let clustering_bonus = avg_similarity * density_bonus;
324
325        Ok(clustering_bonus.max(0.0).min(1.0))
326    }
327
328    /// Calculate context-dependent memory boost
329    ///
330    /// Implements context-dependent memory effects (Godden & Baddeley, 1975)
331    /// where environmental context at encoding and retrieval affects recall.
332    fn calculate_context_boost(&self, memory: &Memory, context: &RetrievalContext) -> Result<f64> {
333        // Extract environmental context from memory metadata
334        let memory_context = memory
335            .metadata
336            .get("environmental_context")
337            .and_then(|v| v.as_object())
338            .map(|obj| {
339                obj.iter()
340                    .filter_map(|(k, v)| v.as_f64().map(|val| (k.clone(), val)))
341                    .collect::<HashMap<String, f64>>()
342            })
343            .unwrap_or_default();
344
345        if memory_context.is_empty() || context.environmental_factors.is_empty() {
346            return Ok(0.0);
347        }
348
349        // Calculate context similarity using overlapping factors
350        let mut context_similarity = 0.0;
351        let mut matching_factors = 0;
352
353        for (factor, current_value) in &context.environmental_factors {
354            if let Some(memory_value) = memory_context.get(factor) {
355                let factor_similarity = 1.0 - (current_value - memory_value).abs().min(1.0);
356                context_similarity += factor_similarity;
357                matching_factors += 1;
358            }
359        }
360
361        if matching_factors == 0 {
362            return Ok(0.0);
363        }
364
365        let avg_context_similarity = context_similarity / matching_factors as f64;
366        let context_boost = avg_context_similarity * self.config.context_weight;
367
368        Ok(context_boost.max(0.0).min(0.5))
369    }
370
371    /// Calculate interference penalty from similar memories
372    ///
373    /// Implements interference theory where similar memories can compete
374    /// and reduce recall probability.
375    fn calculate_interference_penalty(
376        &self,
377        memory: &Memory,
378        similar_memories: &[Memory],
379    ) -> Result<f64> {
380        if similar_memories.is_empty() || memory.embedding.is_none() {
381            return Ok(0.0);
382        }
383
384        let memory_embedding = memory.embedding.as_ref().unwrap();
385        let mut interference_total = 0.0;
386
387        for similar_memory in similar_memories {
388            if similar_memory.id == memory.id {
389                continue; // Skip self
390            }
391
392            if let Some(similar_embedding) = &similar_memory.embedding {
393                let similarity =
394                    self.calculate_cosine_similarity(memory_embedding, similar_embedding)?;
395
396                // Higher similarity with stronger memories creates more interference
397                let strength_ratio =
398                    similar_memory.consolidation_strength / memory.consolidation_strength;
399                let interference_strength = similarity * strength_ratio.min(2.0);
400
401                interference_total += interference_strength;
402            }
403        }
404
405        // Interference is logarithmically scaled to prevent excessive penalties
406        let interference_penalty = (1.0 + interference_total).ln() / 10.0;
407
408        Ok(interference_penalty.max(0.0).min(0.3))
409    }
410
411    /// Combine all cognitive factors into enhanced strength increment
412    fn calculate_enhanced_strength_increment(
413        &self,
414        memory: &Memory,
415        spacing_effect: f64,
416        testing_effect: f64,
417        clustering_bonus: f64,
418        context_boost: f64,
419        interference_penalty: f64,
420    ) -> Result<f64> {
421        // Base increment using hyperbolic tangent growth
422        let base_increment = if let Some(last_access) = memory.last_accessed_at {
423            let hours_since_access =
424                Utc::now().signed_duration_since(last_access).num_seconds() as f64 / 3600.0;
425            let base = (1.0 - (-self.config.beta * hours_since_access).exp())
426                / (1.0 + (-self.config.beta * hours_since_access).exp());
427            self.config.alpha * base
428        } else {
429            self.config.alpha * 0.5 // Default for never-accessed memories
430        };
431
432        // Apply cognitive factors multiplicatively
433        let cognitive_multiplier = spacing_effect
434            * testing_effect
435            * (1.0 + clustering_bonus + context_boost - interference_penalty);
436
437        let enhanced_increment = base_increment * cognitive_multiplier;
438
439        // Ensure reasonable bounds
440        Ok(enhanced_increment.max(0.01).min(2.0))
441    }
442
443    /// Calculate cosine similarity between two vectors
444    fn calculate_cosine_similarity(&self, vec1: &Vector, vec2: &Vector) -> Result<f64> {
445        let slice1 = vec1.as_slice();
446        let slice2 = vec2.as_slice();
447
448        if slice1.len() != slice2.len() {
449            return Err(MemoryError::InvalidRequest {
450                message: "Vector dimensions must match for similarity calculation".to_string(),
451            });
452        }
453
454        let dot_product: f64 = slice1
455            .iter()
456            .zip(slice2.iter())
457            .map(|(a, b)| (*a as f64) * (*b as f64))
458            .sum();
459
460        let norm1: f64 = slice1
461            .iter()
462            .map(|x| (*x as f64).powi(2))
463            .sum::<f64>()
464            .sqrt();
465        let norm2: f64 = slice2
466            .iter()
467            .map(|x| (*x as f64).powi(2))
468            .sum::<f64>()
469            .sqrt();
470
471        if norm1 == 0.0 || norm2 == 0.0 {
472            return Ok(0.0);
473        }
474
475        Ok(dot_product / (norm1 * norm2))
476    }
477
478    /// Update memory with cognitive consolidation results
479    pub async fn apply_consolidation_results(
480        &self,
481        memory: &mut Memory,
482        result: &CognitiveConsolidationResult,
483        repository: &crate::memory::repository::MemoryRepository,
484    ) -> Result<()> {
485        let previous_strength = memory.consolidation_strength;
486        let previous_probability = memory.recall_probability;
487
488        // Update memory fields
489        memory.consolidation_strength = result.new_consolidation_strength;
490        memory.recall_probability = Some(result.recall_probability);
491        memory.access_count += 1;
492        memory.last_accessed_at = Some(Utc::now());
493
494        // Create recall interval
495        let recall_interval = if let Some(last_access) = memory.last_accessed_at {
496            let duration = Utc::now().signed_duration_since(last_access);
497            PgInterval {
498                months: 0,
499                days: duration.num_days() as i32,
500                microseconds: (duration.num_microseconds().unwrap_or(0) % (24 * 60 * 60 * 1000000)),
501            }
502        } else {
503            PgInterval {
504                months: 0,
505                days: 0,
506                microseconds: 0,
507            }
508        };
509
510        memory.last_recall_interval = Some(recall_interval);
511
512        // Log cognitive consolidation event
513        let context = serde_json::json!({
514            "cognitive_factors": result.cognitive_factors,
515            "spacing_bonus": result.spacing_bonus,
516            "difficulty_bonus": result.difficulty_bonus,
517            "context_similarity": result.context_similarity,
518            "calculation_time_ms": result.calculation_time_ms
519        });
520
521        repository
522            .log_consolidation_event(
523                memory.id,
524                "cognitive_consolidation",
525                previous_strength,
526                result.new_consolidation_strength,
527                previous_probability,
528                Some(result.recall_probability),
529                Some(recall_interval),
530                context,
531            )
532            .await?;
533
534        info!(
535            "Applied cognitive consolidation to memory {}: strength {:.3} -> {:.3}, recall {:.3}",
536            memory.id,
537            previous_strength,
538            result.new_consolidation_strength,
539            result.recall_probability
540        );
541
542        Ok(())
543    }
544}
545
546/// Cognitive consolidation service for batch processing
547pub struct CognitiveConsolidationService {
548    engine: CognitiveConsolidationEngine,
549    repository: std::sync::Arc<crate::memory::repository::MemoryRepository>,
550}
551
552impl CognitiveConsolidationService {
553    pub fn new(
554        config: CognitiveConsolidationConfig,
555        repository: std::sync::Arc<crate::memory::repository::MemoryRepository>,
556    ) -> Self {
557        Self {
558            engine: CognitiveConsolidationEngine::new(config),
559            repository,
560        }
561    }
562
563    /// Process consolidation for a batch of memories with cognitive enhancements
564    pub async fn process_batch_consolidation(
565        &self,
566        memory_ids: &[Uuid],
567        context: &RetrievalContext,
568    ) -> Result<Vec<CognitiveConsolidationResult>> {
569        let mut results = Vec::with_capacity(memory_ids.len());
570
571        for &memory_id in memory_ids {
572            match self
573                .process_single_memory_consolidation(memory_id, context)
574                .await
575            {
576                Ok(result) => results.push(result),
577                Err(e) => {
578                    warn!(
579                        "Failed to process consolidation for memory {}: {}",
580                        memory_id, e
581                    );
582                    // Continue processing other memories
583                }
584            }
585        }
586
587        Ok(results)
588    }
589
590    async fn process_single_memory_consolidation(
591        &self,
592        memory_id: Uuid,
593        context: &RetrievalContext,
594    ) -> Result<CognitiveConsolidationResult> {
595        // Get the target memory
596        let mut memory = self.repository.get_memory(memory_id).await?;
597
598        // Find similar memories for clustering analysis
599        let similar_memories = self.find_similar_memories(&memory).await?;
600
601        // Calculate cognitive consolidation
602        let result = self
603            .engine
604            .calculate_cognitive_consolidation(&memory, context, &similar_memories)
605            .await?;
606
607        // Apply results to memory
608        self.engine
609            .apply_consolidation_results(&mut memory, &result, &self.repository)
610            .await?;
611
612        Ok(result)
613    }
614
615    async fn find_similar_memories(&self, memory: &Memory) -> Result<Vec<Memory>> {
616        if memory.embedding.is_none() {
617            return Ok(Vec::new());
618        }
619
620        // Find memories with similar embeddings in the same tier or adjacent tiers
621        let search_request = SearchRequest {
622            query_embedding: Some(memory.embedding.as_ref().unwrap().as_slice().to_vec()),
623            search_type: Some(SearchType::Semantic),
624            similarity_threshold: Some(0.7),
625            limit: Some(20),
626            tier: None, // Search across tiers
627            ..Default::default()
628        };
629
630        let search_response = self.repository.search_memories(search_request).await?;
631
632        Ok(search_response
633            .results
634            .into_iter()
635            .filter(|result| result.memory.id != memory.id) // Exclude self
636            .map(|result| result.memory)
637            .collect())
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644    use chrono::Duration;
645
646    fn create_test_memory() -> Memory {
647        let mut memory = Memory::default();
648        memory.consolidation_strength = 2.0;
649        memory.access_count = 3;
650        memory.last_accessed_at = Some(Utc::now() - Duration::hours(2));
651        memory.importance_score = 0.7;
652        memory
653    }
654
655    fn create_test_context() -> RetrievalContext {
656        RetrievalContext {
657            query_embedding: None,
658            environmental_factors: HashMap::new(),
659            retrieval_latency_ms: 1500, // Optimal difficulty
660            confidence_score: 0.8,
661            related_memories: Vec::new(),
662        }
663    }
664
665    #[test]
666    fn test_spacing_effect_calculation() {
667        let engine = CognitiveConsolidationEngine::new(CognitiveConsolidationConfig::default());
668        let memory = create_test_memory();
669
670        let spacing_effect = engine.calculate_spacing_effect(&memory).unwrap();
671
672        // Should be positive for memory accessed 2 hours ago
673        assert!(spacing_effect > 0.0);
674        assert!(spacing_effect <= 2.0);
675    }
676
677    #[test]
678    fn test_testing_effect_calculation() {
679        let engine = CognitiveConsolidationEngine::new(CognitiveConsolidationConfig::default());
680        let context = create_test_context();
681
682        let testing_effect = engine.calculate_testing_effect(&context).unwrap();
683
684        // Should be positive for optimal difficulty retrieval
685        assert!(testing_effect > 0.0);
686        assert!(testing_effect <= 2.0);
687    }
688
689    #[test]
690    fn test_cognitive_factors_bounds() {
691        let config = CognitiveConsolidationConfig::default();
692
693        // Verify all parameters are within reasonable cognitive ranges
694        assert!(config.alpha > 0.0 && config.alpha < 1.0);
695        assert!(config.beta > 0.0 && config.beta < 5.0);
696        assert!(config.context_weight >= 0.0 && config.context_weight <= 1.0);
697        assert!(config.clustering_threshold > 0.5 && config.clustering_threshold <= 1.0);
698    }
699}