Skip to main content

microscope_memory/
dream.rs

1//! Dream Consolidation for Microscope Memory.
2//!
3//! An offline process that replays the day's recall patterns during idle time,
4//! strengthening important pathways and pruning weak ones — analogous to how
5//! biological sleep consolidates memories.
6//!
7//! Binary format: dream_log.bin (DRM1)
8
9use std::fs;
10use std::path::Path;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use crate::hebbian::HebbianState;
14use crate::predictive_cache::PredictiveCache;
15use crate::resonance::ResonanceState;
16use crate::thought_graph::ThoughtGraphState;
17
18// ─── Constants ──────────────────────────────────────
19
20/// Replay window: consider fingerprints from the last 24h.
21const REPLAY_WINDOW_MS: u64 = 86_400_000;
22
23/// Co-activation pairs seen only this many times AND older than PRUNE_AGE are pruned.
24const COACTIVATION_PRUNE_THRESHOLD: u32 = 1;
25
26/// Prune age: 48h.
27const PRUNE_AGE_MS: u64 = 172_800_000;
28
29/// Activation records with energy below this are pruned (zeroed).
30const ACTIVATION_PRUNE_ENERGY: f32 = 0.001;
31
32/// Dream replay gives partial energy (lighter than real activation).
33const REPLAY_ENERGY: f32 = 0.3;
34
35/// Co-activation pairs seen in 3+ replayed fingerprints get strengthened.
36const STRENGTHEN_MIN_APPEARANCES: usize = 3;
37
38/// Multiplier for strengthened co-activation pairs.
39const STRENGTHEN_MULTIPLIER: f32 = 1.5;
40
41/// Resonance field decay factor during dream.
42const FIELD_DREAM_DECAY: f32 = 0.8;
43
44// ─── Types ──────────────────────────────────────────
45
46/// Record of a single dream consolidation cycle.
47#[derive(Clone, Debug)]
48pub struct DreamCycle {
49    pub timestamp_ms: u64,
50    pub duration_ms: u32,
51    pub replayed_fingerprints: u32,
52    pub strengthened_pairs: u32,
53    pub pruned_pairs: u32,
54    pub pruned_activations: u32,
55    pub consolidated_patterns: u32,
56    pub energy_before: f32,
57    pub energy_after: f32,
58}
59
60/// Persistent dream consolidation log.
61pub struct DreamState {
62    pub cycles: Vec<DreamCycle>,
63    pub last_dream_ms: u64,
64}
65
66pub struct DreamStats {
67    pub total_cycles: usize,
68    pub last_dream_ms: u64,
69    pub total_pruned_pairs: u64,
70    pub total_pruned_activations: u64,
71    pub total_strengthened: u64,
72    pub total_replayed: u64,
73}
74
75// ─── DreamState I/O ─────────────────────────────────
76
77const CYCLE_BYTES: usize = 40; // 8+4+4+4+4+4+4+4+4
78
79impl DreamState {
80    pub fn load_or_init(output_dir: &Path) -> Self {
81        let path = output_dir.join("dream_log.bin");
82        if let Ok(data) = fs::read(&path) {
83            if data.len() >= 16 && &data[0..4] == b"DRM1" {
84                let cycle_count = read_u32(&data, 4) as usize;
85                let last_dream_ms = read_u64(&data, 8);
86                let mut cycles = Vec::with_capacity(cycle_count);
87                for i in 0..cycle_count {
88                    let off = 16 + i * CYCLE_BYTES;
89                    if off + CYCLE_BYTES > data.len() {
90                        break;
91                    }
92                    cycles.push(DreamCycle {
93                        timestamp_ms: read_u64(&data, off),
94                        duration_ms: read_u32(&data, off + 8),
95                        replayed_fingerprints: read_u32(&data, off + 12),
96                        strengthened_pairs: read_u32(&data, off + 16),
97                        pruned_pairs: read_u32(&data, off + 20),
98                        pruned_activations: read_u32(&data, off + 24),
99                        consolidated_patterns: read_u32(&data, off + 28),
100                        energy_before: read_f32(&data, off + 32),
101                        energy_after: read_f32(&data, off + 36),
102                    });
103                }
104                return Self {
105                    cycles,
106                    last_dream_ms,
107                };
108            }
109        }
110        Self {
111            cycles: Vec::new(),
112            last_dream_ms: 0,
113        }
114    }
115
116    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
117        let path = output_dir.join("dream_log.bin");
118        let mut buf = Vec::with_capacity(16 + self.cycles.len() * CYCLE_BYTES);
119        buf.extend_from_slice(b"DRM1");
120        buf.extend_from_slice(&(self.cycles.len() as u32).to_le_bytes());
121        buf.extend_from_slice(&self.last_dream_ms.to_le_bytes());
122        for c in &self.cycles {
123            buf.extend_from_slice(&c.timestamp_ms.to_le_bytes());
124            buf.extend_from_slice(&c.duration_ms.to_le_bytes());
125            buf.extend_from_slice(&c.replayed_fingerprints.to_le_bytes());
126            buf.extend_from_slice(&c.strengthened_pairs.to_le_bytes());
127            buf.extend_from_slice(&c.pruned_pairs.to_le_bytes());
128            buf.extend_from_slice(&c.pruned_activations.to_le_bytes());
129            buf.extend_from_slice(&c.consolidated_patterns.to_le_bytes());
130            buf.extend_from_slice(&c.energy_before.to_le_bytes());
131            buf.extend_from_slice(&c.energy_after.to_le_bytes());
132        }
133        fs::write(&path, &buf).map_err(|e| format!("write dream_log.bin: {}", e))
134    }
135
136    pub fn stats(&self) -> DreamStats {
137        DreamStats {
138            total_cycles: self.cycles.len(),
139            last_dream_ms: self.last_dream_ms,
140            total_pruned_pairs: self.cycles.iter().map(|c| c.pruned_pairs as u64).sum(),
141            total_pruned_activations: self
142                .cycles
143                .iter()
144                .map(|c| c.pruned_activations as u64)
145                .sum(),
146            total_strengthened: self
147                .cycles
148                .iter()
149                .map(|c| c.strengthened_pairs as u64)
150                .sum(),
151            total_replayed: self
152                .cycles
153                .iter()
154                .map(|c| c.replayed_fingerprints as u64)
155                .sum(),
156        }
157    }
158}
159
160// ─── Dream Consolidation ─────────────────────────────
161
162/// Run a full dream consolidation cycle.
163/// 1. Replay recent fingerprints (partial energy boost)
164/// 2. Strengthen co-activation pairs appearing in 3+ replayed fingerprints
165/// 3. Prune weak co-activation pairs (count=1, older than 48h)
166/// 4. Prune cold activation records (zero energy, zero count)
167/// 5. Detect thought patterns across recent sessions
168/// 6. Decay resonance field
169/// 7. Clean up expired predictive cache entries
170pub fn dream_consolidate(output_dir: &Path, block_count: usize) -> Result<DreamCycle, String> {
171    let t0 = now_ms();
172
173    let mut hebb = HebbianState::load_or_init(output_dir, block_count);
174    let mut thought_graph = ThoughtGraphState::load_or_init(output_dir);
175    let mut pred_cache = PredictiveCache::load_or_init(output_dir);
176    let mut resonance = ResonanceState::load_or_init(output_dir);
177
178    // Measure energy before
179    let energy_before: f32 = hebb.activations.iter().map(|r| r.energy).sum();
180
181    // Step 1: Replay recent fingerprints
182    let cutoff = t0.saturating_sub(REPLAY_WINDOW_MS);
183    let recent_fps: Vec<_> = hebb
184        .fingerprints
185        .iter()
186        .filter(|fp| fp.timestamp_ms >= cutoff)
187        .cloned()
188        .collect();
189    let replayed_count = recent_fps.len() as u32;
190
191    // Count how many fingerprints each co-activation pair appears in
192    let mut pair_appearances: std::collections::HashMap<(u32, u32), usize> =
193        std::collections::HashMap::new();
194
195    for fp in &recent_fps {
196        // Replay: partial energy boost
197        for &(block_idx, _score) in &fp.activations {
198            let idx = block_idx as usize;
199            if idx < hebb.activations.len() {
200                let rec = &mut hebb.activations[idx];
201                // Boost energy, but lighter than real activation
202                rec.energy = (rec.energy + REPLAY_ENERGY).min(1.0);
203            }
204        }
205
206        // Track co-activation pair appearances
207        for i in 0..fp.activations.len() {
208            for j in (i + 1)..fp.activations.len() {
209                let a = fp.activations[i].0.min(fp.activations[j].0);
210                let b = fp.activations[i].0.max(fp.activations[j].0);
211                *pair_appearances.entry((a, b)).or_insert(0) += 1;
212            }
213        }
214    }
215
216    // Step 2: Strengthen frequently co-appearing pairs
217    let mut strengthened = 0u32;
218    for ((a, b), appearances) in &pair_appearances {
219        if *appearances >= STRENGTHEN_MIN_APPEARANCES {
220            if let Some(pair) = hebb.coactivations.get_mut(&(*a, *b)) {
221                pair.count = (pair.count as f32 * STRENGTHEN_MULTIPLIER) as u32;
222                strengthened += 1;
223            }
224        }
225    }
226
227    // Step 3: Prune weak co-activation pairs
228    let mut pruned_pairs = 0u32;
229    hebb.coactivations.retain(|_, pair| {
230        if pair.count <= COACTIVATION_PRUNE_THRESHOLD && pair.last_ts_ms + PRUNE_AGE_MS < t0 {
231            pruned_pairs += 1;
232            false
233        } else {
234            true
235        }
236    });
237
238    // Step 4: Prune cold activations
239    let mut pruned_activations = 0u32;
240    for rec in &mut hebb.activations {
241        if rec.energy < ACTIVATION_PRUNE_ENERGY && rec.activation_count == 0 {
242            *rec = crate::hebbian::ActivationRecord::default();
243            pruned_activations += 1;
244        }
245    }
246
247    // Step 5: Pattern detection
248    let patterns_before = thought_graph.crystallized_count();
249    thought_graph.detect_patterns();
250    let consolidated_patterns = (thought_graph.crystallized_count() - patterns_before) as u32;
251
252    // Step 6: Decay resonance field
253    resonance.decay_field(FIELD_DREAM_DECAY);
254    resonance.expire_pulses();
255
256    // Step 7: Predictive cache cleanup — remove predictions with very low confidence
257    pred_cache.dream_cleanup();
258
259    // Measure energy after
260    let energy_after: f32 = hebb.activations.iter().map(|r| r.energy).sum();
261
262    // Save everything
263    hebb.save(output_dir)
264        .map_err(|e| format!("save hebbian: {}", e))?;
265    thought_graph
266        .save(output_dir)
267        .map_err(|e| format!("save thought_graph: {}", e))?;
268    pred_cache
269        .save(output_dir)
270        .map_err(|e| format!("save predictive_cache: {}", e))?;
271    resonance
272        .save(output_dir)
273        .map_err(|e| format!("save resonance: {}", e))?;
274
275    let duration_ms = (now_ms() - t0) as u32;
276
277    Ok(DreamCycle {
278        timestamp_ms: t0,
279        duration_ms,
280        replayed_fingerprints: replayed_count,
281        strengthened_pairs: strengthened,
282        pruned_pairs,
283        pruned_activations,
284        consolidated_patterns,
285        energy_before,
286        energy_after,
287    })
288}
289
290// ─── Binary helpers ─────────────────────────────────
291
292fn read_u32(b: &[u8], off: usize) -> u32 {
293    u32::from_le_bytes(b[off..off + 4].try_into().unwrap())
294}
295fn read_u64(b: &[u8], off: usize) -> u64 {
296    u64::from_le_bytes(b[off..off + 8].try_into().unwrap())
297}
298fn read_f32(b: &[u8], off: usize) -> f32 {
299    f32::from_le_bytes(b[off..off + 4].try_into().unwrap())
300}
301
302fn now_ms() -> u64 {
303    SystemTime::now()
304        .duration_since(UNIX_EPOCH)
305        .unwrap_or_default()
306        .as_millis() as u64
307}
308
309// ─── Tests ──────────────────────────────────────────
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::archetype::ArchetypeState;
315    use crate::hebbian::{ActivationFingerprint, ActivationRecord, CoactivationPair};
316    use crate::predictive_cache::PredictiveCache;
317    use crate::resonance::ResonanceState;
318    use crate::thought_graph::ThoughtGraphState;
319    use std::collections::HashMap;
320
321    fn make_hebb(block_count: usize) -> HebbianState {
322        HebbianState {
323            activations: vec![ActivationRecord::default(); block_count],
324            coactivations: HashMap::new(),
325            fingerprints: Vec::new(),
326        }
327    }
328
329    #[test]
330    fn test_dream_log_roundtrip() {
331        let tmp = tempfile::tempdir().expect("tempdir");
332        let state = DreamState {
333            cycles: vec![
334                DreamCycle {
335                    timestamp_ms: 1000,
336                    duration_ms: 50,
337                    replayed_fingerprints: 10,
338                    strengthened_pairs: 3,
339                    pruned_pairs: 5,
340                    pruned_activations: 2,
341                    consolidated_patterns: 1,
342                    energy_before: 10.5,
343                    energy_after: 8.2,
344                },
345                DreamCycle {
346                    timestamp_ms: 2000,
347                    duration_ms: 30,
348                    replayed_fingerprints: 8,
349                    strengthened_pairs: 2,
350                    pruned_pairs: 3,
351                    pruned_activations: 1,
352                    consolidated_patterns: 0,
353                    energy_before: 8.2,
354                    energy_after: 7.0,
355                },
356            ],
357            last_dream_ms: 2000,
358        };
359        state.save(tmp.path()).unwrap();
360        let loaded = DreamState::load_or_init(tmp.path());
361        assert_eq!(loaded.cycles.len(), 2);
362        assert_eq!(loaded.last_dream_ms, 2000);
363        assert_eq!(loaded.cycles[0].replayed_fingerprints, 10);
364        assert_eq!(loaded.cycles[1].pruned_pairs, 3);
365    }
366
367    #[test]
368    fn test_dream_strengthens_repeated_coactivations() {
369        let tmp = tempfile::tempdir().expect("tempdir");
370        let mut hebb = make_hebb(10);
371
372        // Insert a co-activation pair
373        hebb.coactivations.insert(
374            (0, 1),
375            CoactivationPair {
376                block_a: 0,
377                block_b: 1,
378                count: 5,
379                last_ts_ms: now_ms(),
380            },
381        );
382
383        // Add 3 fingerprints that co-activate blocks 0 and 1
384        let now = now_ms();
385        for i in 0..3 {
386            hebb.fingerprints.push(ActivationFingerprint {
387                timestamp_ms: now - i * 1000,
388                query_hash: 100 + i,
389                activations: vec![(0, 0.5), (1, 0.3)],
390            });
391        }
392
393        hebb.save(tmp.path()).unwrap();
394
395        // Also need thought_graph, pred_cache, resonance, archetypes
396        let tg = ThoughtGraphState::load_or_init(tmp.path());
397        tg.save(tmp.path()).unwrap();
398        let pc = PredictiveCache::load_or_init(tmp.path());
399        pc.save(tmp.path()).unwrap();
400        let res = ResonanceState::load_or_init(tmp.path());
401        res.save(tmp.path()).unwrap();
402        let arc = ArchetypeState::load_or_init(tmp.path());
403        arc.save(tmp.path()).unwrap();
404
405        let cycle = dream_consolidate(tmp.path(), 10).unwrap();
406        assert!(cycle.strengthened_pairs > 0);
407
408        // Verify the pair was strengthened
409        let hebb2 = HebbianState::load_or_init(tmp.path(), 10);
410        let pair = hebb2.coactivations.get(&(0, 1)).unwrap();
411        assert!(pair.count > 5); // was 5, should be 5 * 1.5 = 7
412    }
413
414    #[test]
415    fn test_dream_prunes_weak_pairs() {
416        let tmp = tempfile::tempdir().expect("tempdir");
417        let mut hebb = make_hebb(5);
418
419        // Old, weak pair
420        hebb.coactivations.insert(
421            (0, 1),
422            CoactivationPair {
423                block_a: 0,
424                block_b: 1,
425                count: 1,
426                last_ts_ms: 1000, // very old
427            },
428        );
429        // Recent, strong pair
430        hebb.coactivations.insert(
431            (2, 3),
432            CoactivationPair {
433                block_a: 2,
434                block_b: 3,
435                count: 10,
436                last_ts_ms: now_ms(),
437            },
438        );
439
440        hebb.save(tmp.path()).unwrap();
441        ThoughtGraphState::load_or_init(tmp.path())
442            .save(tmp.path())
443            .unwrap();
444        PredictiveCache::load_or_init(tmp.path())
445            .save(tmp.path())
446            .unwrap();
447        ResonanceState::load_or_init(tmp.path())
448            .save(tmp.path())
449            .unwrap();
450        ArchetypeState::load_or_init(tmp.path())
451            .save(tmp.path())
452            .unwrap();
453
454        let cycle = dream_consolidate(tmp.path(), 5).unwrap();
455        assert_eq!(cycle.pruned_pairs, 1);
456
457        let hebb2 = HebbianState::load_or_init(tmp.path(), 5);
458        assert!(!hebb2.coactivations.contains_key(&(0, 1))); // pruned
459        assert!(hebb2.coactivations.contains_key(&(2, 3))); // kept
460    }
461
462    #[test]
463    fn test_dream_replays_fingerprints() {
464        let tmp = tempfile::tempdir().expect("tempdir");
465        let mut hebb = make_hebb(5);
466
467        // Block 0 has zero energy
468        assert_eq!(hebb.activations[0].energy, 0.0);
469
470        // Add a recent fingerprint activating block 0
471        hebb.fingerprints.push(ActivationFingerprint {
472            timestamp_ms: now_ms() - 1000,
473            query_hash: 42,
474            activations: vec![(0, 0.5)],
475        });
476
477        hebb.save(tmp.path()).unwrap();
478        ThoughtGraphState::load_or_init(tmp.path())
479            .save(tmp.path())
480            .unwrap();
481        PredictiveCache::load_or_init(tmp.path())
482            .save(tmp.path())
483            .unwrap();
484        ResonanceState::load_or_init(tmp.path())
485            .save(tmp.path())
486            .unwrap();
487        ArchetypeState::load_or_init(tmp.path())
488            .save(tmp.path())
489            .unwrap();
490
491        let cycle = dream_consolidate(tmp.path(), 5).unwrap();
492        assert_eq!(cycle.replayed_fingerprints, 1);
493
494        let hebb2 = HebbianState::load_or_init(tmp.path(), 5);
495        assert!(hebb2.activations[0].energy >= REPLAY_ENERGY - 0.01);
496    }
497
498    #[test]
499    fn test_dream_no_fingerprints() {
500        let tmp = tempfile::tempdir().expect("tempdir");
501        let hebb = make_hebb(5);
502        hebb.save(tmp.path()).unwrap();
503        ThoughtGraphState::load_or_init(tmp.path())
504            .save(tmp.path())
505            .unwrap();
506        PredictiveCache::load_or_init(tmp.path())
507            .save(tmp.path())
508            .unwrap();
509        ResonanceState::load_or_init(tmp.path())
510            .save(tmp.path())
511            .unwrap();
512        ArchetypeState::load_or_init(tmp.path())
513            .save(tmp.path())
514            .unwrap();
515
516        let cycle = dream_consolidate(tmp.path(), 5).unwrap();
517        assert_eq!(cycle.replayed_fingerprints, 0);
518        assert_eq!(cycle.strengthened_pairs, 0);
519        assert_eq!(cycle.pruned_pairs, 0);
520    }
521
522    #[test]
523    fn test_dream_stats() {
524        let state = DreamState {
525            cycles: vec![
526                DreamCycle {
527                    timestamp_ms: 1000,
528                    duration_ms: 50,
529                    replayed_fingerprints: 10,
530                    strengthened_pairs: 3,
531                    pruned_pairs: 5,
532                    pruned_activations: 2,
533                    consolidated_patterns: 1,
534                    energy_before: 10.0,
535                    energy_after: 8.0,
536                },
537                DreamCycle {
538                    timestamp_ms: 2000,
539                    duration_ms: 30,
540                    replayed_fingerprints: 8,
541                    strengthened_pairs: 2,
542                    pruned_pairs: 3,
543                    pruned_activations: 1,
544                    consolidated_patterns: 0,
545                    energy_before: 8.0,
546                    energy_after: 7.0,
547                },
548            ],
549            last_dream_ms: 2000,
550        };
551        let stats = state.stats();
552        assert_eq!(stats.total_cycles, 2);
553        assert_eq!(stats.total_pruned_pairs, 8);
554        assert_eq!(stats.total_strengthened, 5);
555        assert_eq!(stats.total_replayed, 18);
556    }
557}