1use 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, pub block_indices: Vec<u32>, pub binding_strength: f32, 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, 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>, 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 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 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, replay_count: 0,
74 };
75
76 self.episodes.push(episode);
77 self.consolidation_queue.push(episode_id);
78
79 episode_id
80 }
81
82 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 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 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 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 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 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 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 pub fn decay(&mut self) {
200 let now = Self::now_ms();
201 const DECAY_THRESHOLD_MS: u64 = 2_592_000_000; 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; }
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 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 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 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 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 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}