Skip to main content

microscope_memory/
hippocampus.rs

1//! Hippocampus — episodic binding, context binding, and consolidation orchestration
2//! Mimics biological hippocampus: indexes new memories, creates context-event bindings,
3//! coordinates transfer from short-term (WM) to long-term (cortex-like) storage
4
5use std::collections::HashMap;
6use std::fs;
7use std::path::Path;
8
9#[derive(Clone, Debug)]
10pub struct ContextBinding {
11    pub query_hash: u64,
12    pub context: String,         // spatial/temporal context
13    pub block_indices: Vec<u32>, // blocks retrieved in this context
14    pub binding_strength: f32,   // 0.0-1.0
15    pub timestamp_ms: u64,
16    pub retrieval_count: u32,
17}
18
19#[derive(Clone, Debug)]
20pub struct EpisodicIndex {
21    pub episode_id: u64,
22    pub blocks: Vec<u32>,
23    pub context_binding: ContextBinding,
24    pub consolidation_state: u8, // 0=fresh, 1=processing, 2=consolidated
25    pub replay_count: u32,
26}
27
28pub struct Hippocampus {
29    pub context_bindings: HashMap<u64, ContextBinding>,
30    pub episodes: Vec<EpisodicIndex>,
31    pub consolidation_queue: Vec<u64>, // episode IDs to consolidate
32    pub binding_threshold: f32,
33}
34
35impl Default for Hippocampus {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl Hippocampus {
42    pub fn new() -> Self {
43        Self {
44            context_bindings: HashMap::new(),
45            episodes: Vec::new(),
46            consolidation_queue: Vec::new(),
47            binding_threshold: 0.3,
48        }
49    }
50
51    /// Create context-event binding: link blocks to their retrieval context
52    pub fn create_binding(&mut self, query_hash: u64, context: &str, blocks: Vec<u32>) -> u64 {
53        let binding_strength = (blocks.len() as f32 / 100.0).min(1.0);
54
55        let binding = ContextBinding {
56            query_hash,
57            context: context.to_string(),
58            block_indices: blocks.clone(),
59            binding_strength,
60            timestamp_ms: Self::now_ms(),
61            retrieval_count: 1,
62        };
63
64        self.context_bindings.insert(query_hash, binding);
65
66        // Create episode
67        let episode_id = Self::episode_hash(&blocks);
68        let episode = EpisodicIndex {
69            episode_id,
70            blocks,
71            context_binding: self.context_bindings[&query_hash].clone(),
72            consolidation_state: 0, // fresh
73            replay_count: 0,
74        };
75
76        self.episodes.push(episode);
77        self.consolidation_queue.push(episode_id);
78
79        episode_id
80    }
81
82    /// Strengthen context-event binding through repetition
83    pub fn reinforce_binding(&mut self, query_hash: u64, success: bool) {
84        if let Some(binding) = self.context_bindings.get_mut(&query_hash) {
85            binding.retrieval_count += 1;
86            if success {
87                binding.binding_strength = (binding.binding_strength + 0.1).min(1.0);
88            } else {
89                binding.binding_strength = (binding.binding_strength - 0.05).max(0.0);
90            }
91        }
92    }
93
94    /// Get consolidation candidates (fresh episodes with strong bindings)
95    pub fn get_consolidation_candidates(&self, k: usize) -> Vec<EpisodicIndex> {
96        let mut candidates: Vec<_> = self
97            .episodes
98            .iter()
99            .filter(|e| {
100                e.consolidation_state == 0
101                    && e.context_binding.binding_strength > self.binding_threshold
102            })
103            .cloned()
104            .collect();
105
106        candidates.sort_by(|a, b| {
107            b.context_binding
108                .binding_strength
109                .partial_cmp(&a.context_binding.binding_strength)
110                .unwrap()
111        });
112
113        candidates.into_iter().take(k).collect()
114    }
115
116    /// Mark episode as consolidating
117    pub fn mark_consolidating(&mut self, episode_id: u64) {
118        if let Some(ep) = self
119            .episodes
120            .iter_mut()
121            .find(|e| e.episode_id == episode_id)
122        {
123            ep.consolidation_state = 1;
124        }
125    }
126
127    /// Mark episode as consolidated
128    pub fn mark_consolidated(&mut self, episode_id: u64) {
129        if let Some(ep) = self
130            .episodes
131            .iter_mut()
132            .find(|e| e.episode_id == episode_id)
133        {
134            ep.consolidation_state = 2;
135            ep.replay_count += 1;
136        }
137    }
138
139    /// Replay episode for consolidation (simulates sleep consolidation)
140    pub fn replay_episode(&mut self, episode_id: u64) -> Option<Vec<u32>> {
141        self.episodes
142            .iter_mut()
143            .find(|e| e.episode_id == episode_id)
144            .map(|e| {
145                e.replay_count += 1;
146                e.context_binding.binding_strength =
147                    (e.context_binding.binding_strength + 0.05).min(1.0);
148                e.blocks.clone()
149            })
150    }
151
152    /// Get related episodes (same context)
153    pub fn get_related_episodes(&self, episode_id: u64) -> Vec<EpisodicIndex> {
154        let context = self
155            .episodes
156            .iter()
157            .find(|e| e.episode_id == episode_id)
158            .map(|e| e.context_binding.context.clone());
159
160        if let Some(ctx) = context {
161            return self
162                .episodes
163                .iter()
164                .filter(|e| e.context_binding.context == ctx && e.episode_id != episode_id)
165                .cloned()
166                .collect();
167        }
168        Vec::new()
169    }
170
171    /// Get statistics
172    pub fn stats(&self) -> (usize, usize, usize, f32) {
173        let total_bindings = self.context_bindings.len();
174        let total_episodes = self.episodes.len();
175        let consolidated = self
176            .episodes
177            .iter()
178            .filter(|e| e.consolidation_state == 2)
179            .count();
180        let avg_binding_strength = if !self.context_bindings.is_empty() {
181            self.context_bindings
182                .values()
183                .map(|b| b.binding_strength)
184                .sum::<f32>()
185                / self.context_bindings.len() as f32
186        } else {
187            0.0
188        };
189
190        (
191            total_bindings,
192            total_episodes,
193            consolidated,
194            avg_binding_strength,
195        )
196    }
197
198    /// Decay old episodes (biological forgetting)
199    pub fn decay(&mut self) {
200        let now = Self::now_ms();
201        const DECAY_THRESHOLD_MS: u64 = 2_592_000_000; // 30 days
202
203        self.episodes.retain(|e| {
204            let age = now.saturating_sub(e.context_binding.timestamp_ms);
205            if age > DECAY_THRESHOLD_MS && e.consolidation_state != 2 {
206                return false; // Remove unconsolidated old episodes
207            }
208            true
209        });
210
211        self.context_bindings.retain(|_, b| {
212            let age = now.saturating_sub(b.timestamp_ms);
213            age <= DECAY_THRESHOLD_MS || b.retrieval_count >= 5
214        });
215    }
216
217    pub fn save(&self, dir: &Path) -> Result<(), String> {
218        let path = dir.join("hippocampus.bin");
219        let tmp_path = dir.join("hippocampus.bin.tmp");
220        let mut data = Vec::new();
221
222        data.extend_from_slice(b"HIPP");
223        data.push(1);
224
225        // Bindings
226        let binding_count = self.context_bindings.len() as u32;
227        data.extend_from_slice(&binding_count.to_le_bytes());
228        for (hash, binding) in &self.context_bindings {
229            data.extend_from_slice(&hash.to_le_bytes());
230            data.extend_from_slice(&binding.binding_strength.to_le_bytes());
231            data.extend_from_slice(&binding.timestamp_ms.to_le_bytes());
232            data.extend_from_slice(&binding.retrieval_count.to_le_bytes());
233
234            let ctx_bytes = binding.context.as_bytes();
235            data.extend_from_slice(&(ctx_bytes.len() as u16).to_le_bytes());
236            data.extend_from_slice(ctx_bytes);
237
238            let block_count = binding.block_indices.len() as u16;
239            data.extend_from_slice(&block_count.to_le_bytes());
240            for &idx in &binding.block_indices {
241                data.extend_from_slice(&idx.to_le_bytes());
242            }
243        }
244
245        // Episodes
246        let episode_count = self.episodes.len() as u32;
247        data.extend_from_slice(&episode_count.to_le_bytes());
248        for episode in &self.episodes {
249            data.extend_from_slice(&episode.episode_id.to_le_bytes());
250            data.push(episode.consolidation_state);
251            data.extend_from_slice(&episode.replay_count.to_le_bytes());
252
253            let block_count = episode.blocks.len() as u16;
254            data.extend_from_slice(&block_count.to_le_bytes());
255            for &idx in &episode.blocks {
256                data.extend_from_slice(&idx.to_le_bytes());
257            }
258        }
259
260        fs::write(&tmp_path, &data).map_err(|e| e.to_string())?;
261        fs::rename(&tmp_path, &path).map_err(|e| format!("rename hippocampus.bin: {}", e))
262    }
263
264    pub fn load(dir: &Path) -> Result<Self, String> {
265        let path = dir.join("hippocampus.bin");
266        if !path.exists() {
267            return Ok(Self::new());
268        }
269
270        let data = fs::read(&path).map_err(|e| e.to_string())?;
271        if data.len() < 5 || &data[0..4] != b"HIPP" {
272            return Ok(Self::new());
273        }
274
275        let mut idx = 5;
276        let mut context_bindings = HashMap::new();
277        let mut episodes = Vec::new();
278
279        // Read bindings
280        if idx + 4 <= data.len() {
281            let binding_count =
282                u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]])
283                    as usize;
284            idx += 4;
285
286            for _ in 0..binding_count {
287                if idx + 32 > data.len() {
288                    break;
289                }
290
291                let hash = u64::from_le_bytes([
292                    data[idx],
293                    data[idx + 1],
294                    data[idx + 2],
295                    data[idx + 3],
296                    data[idx + 4],
297                    data[idx + 5],
298                    data[idx + 6],
299                    data[idx + 7],
300                ]);
301                idx += 8;
302
303                let binding_strength =
304                    f32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
305                idx += 4;
306
307                let timestamp_ms = u64::from_le_bytes([
308                    data[idx],
309                    data[idx + 1],
310                    data[idx + 2],
311                    data[idx + 3],
312                    data[idx + 4],
313                    data[idx + 5],
314                    data[idx + 6],
315                    data[idx + 7],
316                ]);
317                idx += 8;
318
319                let retrieval_count =
320                    u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
321                idx += 4;
322
323                if idx + 2 > data.len() {
324                    break;
325                }
326                let ctx_len = u16::from_le_bytes([data[idx], data[idx + 1]]) as usize;
327                idx += 2;
328
329                if idx + ctx_len > data.len() {
330                    break;
331                }
332                let context = String::from_utf8_lossy(&data[idx..idx + ctx_len]).to_string();
333                idx += ctx_len;
334
335                if idx + 2 > data.len() {
336                    break;
337                }
338                let block_count = u16::from_le_bytes([data[idx], data[idx + 1]]) as usize;
339                idx += 2;
340
341                let mut block_indices = Vec::new();
342                for _ in 0..block_count {
343                    if idx + 4 > data.len() {
344                        break;
345                    }
346                    let block_idx = u32::from_le_bytes([
347                        data[idx],
348                        data[idx + 1],
349                        data[idx + 2],
350                        data[idx + 3],
351                    ]);
352                    block_indices.push(block_idx);
353                    idx += 4;
354                }
355
356                context_bindings.insert(
357                    hash,
358                    ContextBinding {
359                        query_hash: hash,
360                        context,
361                        block_indices,
362                        binding_strength,
363                        timestamp_ms,
364                        retrieval_count,
365                    },
366                );
367            }
368        }
369
370        // Read episodes
371        if idx + 4 <= data.len() {
372            let episode_count =
373                u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]])
374                    as usize;
375            idx += 4;
376
377            for _ in 0..episode_count {
378                if idx + 15 > data.len() {
379                    break;
380                }
381
382                let episode_id = u64::from_le_bytes([
383                    data[idx],
384                    data[idx + 1],
385                    data[idx + 2],
386                    data[idx + 3],
387                    data[idx + 4],
388                    data[idx + 5],
389                    data[idx + 6],
390                    data[idx + 7],
391                ]);
392                idx += 8;
393
394                let consolidation_state = data[idx];
395                idx += 1;
396
397                let replay_count =
398                    u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
399                idx += 4;
400
401                let block_count = u16::from_le_bytes([data[idx], data[idx + 1]]) as usize;
402                idx += 2;
403
404                let mut blocks = Vec::new();
405                for _ in 0..block_count {
406                    if idx + 4 > data.len() {
407                        break;
408                    }
409                    let block_idx = u32::from_le_bytes([
410                        data[idx],
411                        data[idx + 1],
412                        data[idx + 2],
413                        data[idx + 3],
414                    ]);
415                    blocks.push(block_idx);
416                    idx += 4;
417                }
418
419                // Create dummy binding for loading
420                let binding = ContextBinding {
421                    query_hash: episode_id,
422                    context: String::new(),
423                    block_indices: blocks.clone(),
424                    binding_strength: 0.5,
425                    timestamp_ms: Self::now_ms(),
426                    retrieval_count: 0,
427                };
428
429                episodes.push(EpisodicIndex {
430                    episode_id,
431                    blocks,
432                    context_binding: binding,
433                    consolidation_state,
434                    replay_count,
435                });
436            }
437        }
438
439        Ok(Self {
440            context_bindings,
441            episodes,
442            consolidation_queue: Vec::new(),
443            binding_threshold: 0.3,
444        })
445    }
446
447    pub fn load_or_init(dir: &Path) -> Self {
448        Self::load(dir).unwrap_or_else(|_| Self::new())
449    }
450
451    fn episode_hash(blocks: &[u32]) -> u64 {
452        let mut hash: u64 = 0xcbf29ce484222325;
453        for &block in blocks.iter().take(10) {
454            hash = hash.wrapping_mul(0x100000001b3) ^ (block as u64);
455        }
456        hash
457    }
458
459    fn now_ms() -> u64 {
460        std::time::SystemTime::now()
461            .duration_since(std::time::UNIX_EPOCH)
462            .unwrap_or_default()
463            .as_millis() as u64
464    }
465}