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                    as i64,
502            }
503        } else {
504            PgInterval {
505                months: 0,
506                days: 0,
507                microseconds: 0,
508            }
509        };
510
511        memory.last_recall_interval = Some(recall_interval);
512
513        // Log cognitive consolidation event
514        let context = serde_json::json!({
515            "cognitive_factors": result.cognitive_factors,
516            "spacing_bonus": result.spacing_bonus,
517            "difficulty_bonus": result.difficulty_bonus,
518            "context_similarity": result.context_similarity,
519            "calculation_time_ms": result.calculation_time_ms
520        });
521
522        repository
523            .log_consolidation_event(
524                memory.id,
525                "cognitive_consolidation",
526                previous_strength,
527                result.new_consolidation_strength,
528                previous_probability,
529                Some(result.recall_probability),
530                Some(recall_interval),
531                context,
532            )
533            .await?;
534
535        info!(
536            "Applied cognitive consolidation to memory {}: strength {:.3} -> {:.3}, recall {:.3}",
537            memory.id,
538            previous_strength,
539            result.new_consolidation_strength,
540            result.recall_probability
541        );
542
543        Ok(())
544    }
545}
546
547/// Cognitive consolidation service for batch processing
548pub struct CognitiveConsolidationService {
549    engine: CognitiveConsolidationEngine,
550    repository: std::sync::Arc<crate::memory::repository::MemoryRepository>,
551}
552
553impl CognitiveConsolidationService {
554    pub fn new(
555        config: CognitiveConsolidationConfig,
556        repository: std::sync::Arc<crate::memory::repository::MemoryRepository>,
557    ) -> Self {
558        Self {
559            engine: CognitiveConsolidationEngine::new(config),
560            repository,
561        }
562    }
563
564    /// Process consolidation for a batch of memories with cognitive enhancements
565    pub async fn process_batch_consolidation(
566        &self,
567        memory_ids: &[Uuid],
568        context: &RetrievalContext,
569    ) -> Result<Vec<CognitiveConsolidationResult>> {
570        let mut results = Vec::with_capacity(memory_ids.len());
571
572        for &memory_id in memory_ids {
573            match self
574                .process_single_memory_consolidation(memory_id, context)
575                .await
576            {
577                Ok(result) => results.push(result),
578                Err(e) => {
579                    warn!(
580                        "Failed to process consolidation for memory {}: {}",
581                        memory_id, e
582                    );
583                    // Continue processing other memories
584                }
585            }
586        }
587
588        Ok(results)
589    }
590
591    async fn process_single_memory_consolidation(
592        &self,
593        memory_id: Uuid,
594        context: &RetrievalContext,
595    ) -> Result<CognitiveConsolidationResult> {
596        // Get the target memory
597        let mut memory = self.repository.get_memory(memory_id).await?;
598
599        // Find similar memories for clustering analysis
600        let similar_memories = self.find_similar_memories(&memory).await?;
601
602        // Calculate cognitive consolidation
603        let result = self
604            .engine
605            .calculate_cognitive_consolidation(&memory, context, &similar_memories)
606            .await?;
607
608        // Apply results to memory
609        self.engine
610            .apply_consolidation_results(&mut memory, &result, &self.repository)
611            .await?;
612
613        Ok(result)
614    }
615
616    async fn find_similar_memories(&self, memory: &Memory) -> Result<Vec<Memory>> {
617        if memory.embedding.is_none() {
618            return Ok(Vec::new());
619        }
620
621        // Find memories with similar embeddings in the same tier or adjacent tiers
622        let search_request = SearchRequest {
623            query_embedding: Some(memory.embedding.as_ref().unwrap().as_slice().to_vec()),
624            search_type: Some(SearchType::Semantic),
625            similarity_threshold: Some(0.7),
626            limit: Some(20),
627            tier: None, // Search across tiers
628            ..Default::default()
629        };
630
631        let search_response = self.repository.search_memories(search_request).await?;
632
633        Ok(search_response
634            .results
635            .into_iter()
636            .filter(|result| result.memory.id != memory.id) // Exclude self
637            .map(|result| result.memory)
638            .collect())
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645    use chrono::Duration;
646
647    fn create_test_memory() -> Memory {
648        let mut memory = Memory::default();
649        memory.consolidation_strength = 2.0;
650        memory.access_count = 3;
651        memory.last_accessed_at = Some(Utc::now() - Duration::hours(2));
652        memory.importance_score = 0.7;
653        memory
654    }
655
656    fn create_test_context() -> RetrievalContext {
657        RetrievalContext {
658            query_embedding: None,
659            environmental_factors: HashMap::new(),
660            retrieval_latency_ms: 1500, // Optimal difficulty
661            confidence_score: 0.8,
662            related_memories: Vec::new(),
663        }
664    }
665
666    #[test]
667    fn test_spacing_effect_calculation() {
668        let engine = CognitiveConsolidationEngine::new(CognitiveConsolidationConfig::default());
669        let memory = create_test_memory();
670
671        let spacing_effect = engine.calculate_spacing_effect(&memory).unwrap();
672
673        // Should be positive for memory accessed 2 hours ago
674        assert!(spacing_effect > 0.0);
675        assert!(spacing_effect <= 2.0);
676    }
677
678    #[test]
679    fn test_testing_effect_calculation() {
680        let engine = CognitiveConsolidationEngine::new(CognitiveConsolidationConfig::default());
681        let context = create_test_context();
682
683        let testing_effect = engine.calculate_testing_effect(&context).unwrap();
684
685        // Should be positive for optimal difficulty retrieval
686        assert!(testing_effect > 0.0);
687        assert!(testing_effect <= 2.0);
688    }
689
690    #[test]
691    fn test_cognitive_factors_bounds() {
692        let config = CognitiveConsolidationConfig::default();
693
694        // Verify all parameters are within reasonable cognitive ranges
695        assert!(config.alpha > 0.0 && config.alpha < 1.0);
696        assert!(config.beta > 0.0 && config.beta < 5.0);
697        assert!(config.context_weight >= 0.0 && config.context_weight <= 1.0);
698        assert!(config.clustering_threshold > 0.5 && config.clustering_threshold <= 1.0);
699    }
700}