1use std::fs;
10use std::path::Path;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use crate::hebbian::{ActivationRecord, HebbianState};
14use crate::predictive_cache::PredictiveCache;
15use crate::resonance::ResonanceState;
16use crate::thought_graph::ThoughtGraphState;
17use crate::{emotional_similarity, load_emotion_lookup};
18
19const REPLAY_WINDOW_MS: u64 = 86_400_000;
23
24const COACTIVATION_PRUNE_THRESHOLD: u32 = 1;
26
27const PRUNE_AGE_MS: u64 = 172_800_000;
29
30const ACTIVATION_PRUNE_ENERGY: f32 = 0.001;
32
33const REPLAY_ENERGY: f32 = 0.3;
35
36const STRENGTHEN_MIN_APPEARANCES: usize = 3;
38
39const STRENGTHEN_MULTIPLIER: f32 = 1.5;
41
42const FIELD_DREAM_DECAY: f32 = 0.8;
44
45#[derive(Clone, Debug)]
49pub struct DreamCycle {
50 pub timestamp_ms: u64,
51 pub duration_ms: u32,
52 pub replayed_fingerprints: u32,
53 pub strengthened_pairs: u32,
54 pub pruned_pairs: u32,
55 pub pruned_activations: u32,
56 pub consolidated_patterns: u32,
57 pub forgotten_blocks: u32,
58 pub energy_before: f32,
59 pub energy_after: f32,
60}
61
62pub struct DreamState {
64 pub cycles: Vec<DreamCycle>,
65 pub last_dream_ms: u64,
66}
67
68pub struct DreamStats {
69 pub total_cycles: usize,
70 pub last_dream_ms: u64,
71 pub total_pruned_pairs: u64,
72 pub total_pruned_activations: u64,
73 pub total_strengthened: u64,
74 pub total_replayed: u64,
75 pub total_forgotten_blocks: u64,
76}
77
78const CYCLE_BYTES: usize = 44; impl DreamState {
83 pub fn load_or_init(output_dir: &Path) -> Self {
84 let path = output_dir.join("dream_log.bin");
85 if let Ok(data) = fs::read(&path) {
86 if data.len() >= 16 && &data[0..4] == b"DRM1" {
87 let cycle_count = read_u32(&data, 4) as usize;
88 let last_dream_ms = read_u64(&data, 8);
89 let mut cycles = Vec::with_capacity(cycle_count);
90 for i in 0..cycle_count {
91 let off = 16 + i * CYCLE_BYTES;
92 if off + CYCLE_BYTES > data.len() {
93 break;
94 }
95 cycles.push(DreamCycle {
96 timestamp_ms: read_u64(&data, off),
97 duration_ms: read_u32(&data, off + 8),
98 replayed_fingerprints: read_u32(&data, off + 12),
99 strengthened_pairs: read_u32(&data, off + 16),
100 pruned_pairs: read_u32(&data, off + 20),
101 pruned_activations: read_u32(&data, off + 24),
102 consolidated_patterns: read_u32(&data, off + 28),
103 forgotten_blocks: read_u32(&data, off + 32),
104 energy_before: read_f32(&data, off + 36),
105 energy_after: read_f32(&data, off + 40),
106 });
107 }
108 return Self {
109 cycles,
110 last_dream_ms,
111 };
112 }
113 }
114 Self {
115 cycles: Vec::new(),
116 last_dream_ms: 0,
117 }
118 }
119
120 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
121 let path = output_dir.join("dream_log.bin");
122 let mut buf = Vec::with_capacity(16 + self.cycles.len() * CYCLE_BYTES);
123 buf.extend_from_slice(b"DRM1");
124 buf.extend_from_slice(&(self.cycles.len() as u32).to_le_bytes());
125 buf.extend_from_slice(&self.last_dream_ms.to_le_bytes());
126 for c in &self.cycles {
127 buf.extend_from_slice(&c.timestamp_ms.to_le_bytes());
128 buf.extend_from_slice(&c.duration_ms.to_le_bytes());
129 buf.extend_from_slice(&c.replayed_fingerprints.to_le_bytes());
130 buf.extend_from_slice(&c.strengthened_pairs.to_le_bytes());
131 buf.extend_from_slice(&c.pruned_pairs.to_le_bytes());
132 buf.extend_from_slice(&c.pruned_activations.to_le_bytes());
133 buf.extend_from_slice(&c.consolidated_patterns.to_le_bytes());
134 buf.extend_from_slice(&c.forgotten_blocks.to_le_bytes());
135 buf.extend_from_slice(&c.energy_before.to_le_bytes());
136 buf.extend_from_slice(&c.energy_after.to_le_bytes());
137 }
138 let tmp_path = output_dir.join("dream_log.bin.tmp");
139 fs::write(&tmp_path, &buf).map_err(|e| format!("write dream_log.bin: {}", e))?;
140 fs::rename(&tmp_path, &path).map_err(|e| format!("rename dream_log.bin: {}", e))
141 }
142
143 pub fn stats(&self) -> DreamStats {
144 DreamStats {
145 total_cycles: self.cycles.len(),
146 last_dream_ms: self.last_dream_ms,
147 total_pruned_pairs: self.cycles.iter().map(|c| c.pruned_pairs as u64).sum(),
148 total_pruned_activations: self
149 .cycles
150 .iter()
151 .map(|c| c.pruned_activations as u64)
152 .sum(),
153 total_forgotten_blocks: self.cycles.iter().map(|c| c.forgotten_blocks as u64).sum(),
154 total_strengthened: self
155 .cycles
156 .iter()
157 .map(|c| c.strengthened_pairs as u64)
158 .sum(),
159 total_replayed: self
160 .cycles
161 .iter()
162 .map(|c| c.replayed_fingerprints as u64)
163 .sum(),
164 }
165 }
166}
167
168pub fn dream_consolidate(
179 output_dir: &Path,
180 block_count: usize,
181 max_blocks: usize,
182 protect_min_importance: u8,
183) -> Result<DreamCycle, String> {
184 let t0 = now_ms();
185
186 let mut hebb = HebbianState::load_or_init(output_dir, block_count);
187 let mut thought_graph = ThoughtGraphState::load_or_init(output_dir);
188 let mut pred_cache = PredictiveCache::load_or_init(output_dir);
189 let mut resonance = ResonanceState::load_or_init(output_dir);
190
191 let energy_before: f32 = hebb.activations.iter().map(|r| r.energy).sum();
193
194 let cutoff = t0.saturating_sub(REPLAY_WINDOW_MS);
196 let recent_fps: Vec<_> = hebb
197 .fingerprints
198 .iter()
199 .filter(|fp| fp.timestamp_ms >= cutoff)
200 .cloned()
201 .collect();
202 let replayed_count = recent_fps.len() as u32;
203
204 let mut pair_appearances: std::collections::HashMap<(u32, u32), usize> =
206 std::collections::HashMap::new();
207
208 for fp in &recent_fps {
209 for &(block_idx, _score) in &fp.activations {
211 let idx = block_idx as usize;
212 if idx >= hebb.activations.len() {
217 if idx >= 4_000_000 {
218 continue; }
220 hebb.activations
221 .resize(idx + 1, ActivationRecord::default());
222 }
223 let rec = &mut hebb.activations[idx];
224 rec.energy = (rec.energy + REPLAY_ENERGY).min(1.0);
226 }
227
228 for i in 0..fp.activations.len() {
230 for j in (i + 1)..fp.activations.len() {
231 let a = fp.activations[i].0.min(fp.activations[j].0);
232 let b = fp.activations[i].0.max(fp.activations[j].0);
233 *pair_appearances.entry((a, b)).or_insert(0) += 1;
234 }
235 }
236 }
237
238 let emotion_lookup = load_emotion_lookup(output_dir);
240 let mut strengthened = 0u32;
241 let mut coherence_boosted = 0u32;
242 let mut _emotion_pruned = 0u32;
243
244 for ((a, b), appearances) in &pair_appearances {
245 if *appearances >= STRENGTHEN_MIN_APPEARANCES {
246 let coherence_mult = emotion_lookup
248 .as_ref()
249 .and_then(|lookup| {
250 lookup(*a as usize).and_then(|ea| {
251 lookup(*b as usize).map(|eb| {
252 let sim = emotional_similarity(&ea, &eb);
253 if sim > 0.4 {
254 coherence_boosted += 1;
255 1.0 + sim * 0.5 } else {
257 1.0
258 }
259 })
260 })
261 })
262 .unwrap_or(1.0);
263
264 if let Some(pair) = hebb.coactivations.get_mut(&(*a, *b)) {
265 pair.count = (pair.count as f32 * STRENGTHEN_MULTIPLIER * coherence_mult) as u32;
266 strengthened += 1;
267 }
268 } else if let Some(ref lookup) = emotion_lookup {
269 if let (Some(ea), Some(eb)) = (lookup(*a as usize), lookup(*b as usize)) {
271 let sim = emotional_similarity(&ea, &eb);
272 if sim < 0.1 {
273 if let Some(pair) = hebb.coactivations.get_mut(&(*a, *b)) {
274 if pair.count > 1 {
275 pair.count /= 2;
276 _emotion_pruned += 1;
277 }
278 }
279 }
280 }
281 }
282 }
283
284 let mut pruned_pairs = 0u32;
286 hebb.coactivations.retain(|_, pair| {
287 if pair.count <= COACTIVATION_PRUNE_THRESHOLD && pair.last_ts_ms + PRUNE_AGE_MS < t0 {
288 pruned_pairs += 1;
289 false
290 } else {
291 true
292 }
293 });
294
295 let mut pruned_activations = 0u32;
297 for rec in &mut hebb.activations {
298 if rec.energy < ACTIVATION_PRUNE_ENERGY && rec.activation_count == 0 {
299 *rec = crate::hebbian::ActivationRecord::default();
300 pruned_activations += 1;
301 }
302 }
303
304 let patterns_before = thought_graph.crystallized_count();
306 thought_graph.detect_patterns();
307 let consolidated_patterns = (thought_graph.crystallized_count() - patterns_before) as u32;
308
309 resonance.decay_field(FIELD_DREAM_DECAY);
311 resonance.expire_pulses();
312
313 pred_cache.dream_cleanup();
315
316 let mut forgotten = forget_old_thoughts(output_dir, block_count)?;
318
319 let evicted = evict_over_capacity(output_dir, max_blocks, protect_min_importance)?;
323 if evicted > 0 {
324 println!(
325 " [EVICT] {} blokk eltávolítva (plafon: {}, maradt: {})",
326 evicted,
327 max_blocks,
328 block_count.saturating_sub(evicted as usize)
329 );
330 }
331 forgotten += evicted;
332
333 let energy_after: f32 = hebb.activations.iter().map(|r| r.energy).sum();
335
336 hebb.save(output_dir)
338 .map_err(|e| format!("save hebbian: {}", e))?;
339 thought_graph
340 .save(output_dir)
341 .map_err(|e| format!("save thought_graph: {}", e))?;
342 pred_cache
343 .save(output_dir)
344 .map_err(|e| format!("save predictive_cache: {}", e))?;
345 resonance
346 .save(output_dir)
347 .map_err(|e| format!("save resonance: {}", e))?;
348
349 let duration_ms = (now_ms() - t0) as u32;
350
351 Ok(DreamCycle {
352 timestamp_ms: t0,
353 duration_ms,
354 replayed_fingerprints: replayed_count,
355 strengthened_pairs: strengthened,
356 pruned_pairs,
357 pruned_activations,
358 consolidated_patterns,
359 forgotten_blocks: forgotten,
360 energy_before,
361 energy_after,
362 })
363}
364
365fn block_data_span(headers: &[u8], idx: usize) -> Option<(usize, usize)> {
381 let off = idx * crate::HEADER_SIZE;
382 if off + crate::HEADER_SIZE > headers.len() {
383 return None;
384 }
385 let start = u32::from_le_bytes([
386 headers[off + 18],
387 headers[off + 19],
388 headers[off + 20],
389 headers[off + 21],
390 ]) as usize;
391 let len = u16::from_le_bytes([headers[off + 22], headers[off + 23]]) as usize;
392 Some((start, len))
393}
394
395fn read_u32(b: &[u8], off: usize) -> u32 {
396 u32::from_le_bytes(b[off..off + 4].try_into().unwrap())
397}
398fn read_u64(b: &[u8], off: usize) -> u64 {
399 u64::from_le_bytes(b[off..off + 8].try_into().unwrap())
400}
401fn read_f32(b: &[u8], off: usize) -> f32 {
402 f32::from_le_bytes(b[off..off + 4].try_into().unwrap())
403}
404
405fn now_ms() -> u64 {
406 SystemTime::now()
407 .duration_since(UNIX_EPOCH)
408 .unwrap_or_default()
409 .as_millis() as u64
410}
411
412#[allow(dead_code)]
421const FORGET_AGE_MS: u64 = 86_400_000; const FORGET_INTERNAL_LAYERS: &[u8] = &[2, 3, 6, 11];
423const FORGET_MIN_IMPORTANCE: u8 = 5;
424
425pub fn forget_old_thoughts(output_dir: &Path, _block_count: usize) -> Result<u32, String> {
426 use crate::{BLOCK_DATA_SIZE, DEPTH_ENTRY_SIZE, HEADER_SIZE, META_HEADER_SIZE};
427 use std::fs;
428
429 let hdr_path = output_dir.join("microscope.bin");
430 let dat_path = output_dir.join("data.bin");
431 let meta_path = output_dir.join("meta.bin");
432
433 if !hdr_path.exists() || !dat_path.exists() || !meta_path.exists() {
434 return Ok(0); }
436
437 let headers = fs::read(&hdr_path).map_err(|e| format!("read microscope.bin: {}", e))?;
438 let data = fs::read(&dat_path).map_err(|e| format!("read data.bin: {}", e))?;
439 let meta = fs::read(&meta_path).map_err(|e| format!("read meta.bin: {}", e))?;
440
441 let actual_blocks = headers.len() / HEADER_SIZE;
442 if actual_blocks == 0 {
443 return Ok(0);
444 }
445
446 let _t0 = now_ms();
447 let mut keep_indices: Vec<usize> = Vec::with_capacity(actual_blocks);
448 let mut forgotten = 0u32;
449
450 for i in 0..actual_blocks {
451 let off = i * HEADER_SIZE;
452 if off + HEADER_SIZE > headers.len() {
453 break;
454 }
455
456 let layer_id = headers[off + 17];
458 let importance = headers[off + 48];
460
461 if FORGET_INTERNAL_LAYERS.contains(&layer_id) && importance < FORGET_MIN_IMPORTANCE {
463 forgotten += 1;
468 continue; }
470
471 keep_indices.push(i);
472 }
473
474 if forgotten == 0 {
475 return Ok(0);
476 }
477
478 let mut new_headers = Vec::with_capacity(keep_indices.len() * HEADER_SIZE);
480 let mut new_data = Vec::with_capacity(keep_indices.len() * BLOCK_DATA_SIZE);
481
482 for &idx in &keep_indices {
483 let hdr_off = idx * HEADER_SIZE;
484 new_headers.extend_from_slice(&headers[hdr_off..hdr_off + HEADER_SIZE]);
485
486 if let Some((data_start, data_len)) = block_data_span(&headers, idx) {
494 let data_end = data_start.saturating_add(data_len).min(data.len());
495 if data_start < data_end {
496 new_data.extend_from_slice(&data[data_start..data_end]);
497 }
498 }
499 }
500
501 let hdr_tmp = output_dir.join("microscope.bin.tmp");
502 let dat_tmp = output_dir.join("data.bin.tmp");
503 fs::write(&hdr_tmp, &new_headers).map_err(|e| format!("write microscope.bin: {}", e))?;
504 fs::write(&dat_tmp, &new_data).map_err(|e| format!("write data.bin: {}", e))?;
505 fs::rename(&hdr_tmp, &hdr_path).map_err(|e| format!("rename microscope.bin: {}", e))?;
506 fs::rename(&dat_tmp, &dat_path).map_err(|e| format!("rename data.bin: {}", e))?;
507
508 let n = keep_indices.len();
510 let mut new_meta = Vec::with_capacity(META_HEADER_SIZE + 9 * DEPTH_ENTRY_SIZE);
511
512 if meta.len() >= 8 {
514 new_meta.extend_from_slice(&meta[..8]);
515 } else {
516 new_meta.extend_from_slice(b"MSC4 ");
517 }
518 new_meta.extend_from_slice(&(n as u32).to_le_bytes());
520
521 let mut depth_counts = [0u32; 9];
523 for &idx in &keep_indices {
524 let off = idx * HEADER_SIZE;
525 let depth = headers[off + 14]; if (depth as usize) < 9 {
527 depth_counts[depth as usize] += 1;
528 }
529 }
530
531 let mut running_start = 0u32;
532 for &count in &depth_counts[..9] {
533 new_meta.extend_from_slice(&running_start.to_le_bytes());
534 new_meta.extend_from_slice(&count.to_le_bytes());
535 running_start += count;
536 }
537
538 let meta_tail_start = META_HEADER_SIZE + 9 * DEPTH_ENTRY_SIZE;
540 if meta_tail_start < meta.len() {
541 new_meta.extend_from_slice(&meta[meta_tail_start..]);
542 }
543
544 let meta_tmp = output_dir.join("meta.bin.tmp");
545 fs::write(&meta_tmp, &new_meta).map_err(|e| format!("write meta.bin: {}", e))?;
546 fs::rename(&meta_tmp, &meta_path).map_err(|e| format!("rename meta.bin: {}", e))?;
547
548 println!(
549 " [FORGET] {} belső gondolat elfelejtve ({} blokk maradt)",
550 forgotten, n
551 );
552
553 Ok(forgotten)
554}
555
556pub fn evict_over_capacity(
565 output_dir: &Path,
566 max_blocks: usize,
567 protect_min_importance: u8,
568) -> Result<u32, String> {
569 use crate::{BLOCK_DATA_SIZE, DEPTH_ENTRY_SIZE, HEADER_SIZE, META_HEADER_SIZE};
570
571 let hdr_path = output_dir.join("microscope.bin");
572 let dat_path = output_dir.join("data.bin");
573 let meta_path = output_dir.join("meta.bin");
574 if !hdr_path.exists() || !dat_path.exists() || !meta_path.exists() {
575 return Ok(0);
576 }
577 let headers = fs::read(&hdr_path).map_err(|e| format!("read microscope.bin: {e}"))?;
578 let data = fs::read(&dat_path).map_err(|e| format!("read data.bin: {e}"))?;
579 let meta = fs::read(&meta_path).map_err(|e| format!("read meta.bin: {e}"))?;
580
581 let actual_blocks = headers.len() / HEADER_SIZE;
582 if actual_blocks == 0 || max_blocks == 0 || actual_blocks <= max_blocks {
583 return Ok(0);
584 }
585 let target = actual_blocks - max_blocks;
586
587 let hebb = crate::hebbian::HebbianState::load_or_init(output_dir, actual_blocks);
589
590 let mut scored: Vec<(usize, f32)> = Vec::with_capacity(actual_blocks);
591 for i in 0..actual_blocks {
592 let off = i * HEADER_SIZE;
593 let importance = headers[off + 48]; if importance >= protect_min_importance {
595 continue; }
597 let energy = hebb.activations.get(i).map(|r| r.energy).unwrap_or(0.0);
598 let age = i as f32 / actual_blocks as f32;
599 scored.push((i, importance as f32 * 10.0 + energy - age * 5.0));
600 }
601 if scored.len() <= target {
602 return Ok(0);
604 }
605
606 scored.sort_by(|a, b| a.1.total_cmp(&b.1)); let victims: std::collections::HashSet<usize> =
608 scored.iter().take(target).map(|(idx, _)| *idx).collect();
609 let keep_indices: Vec<usize> = (0..actual_blocks)
610 .filter(|i| !victims.contains(i))
611 .collect();
612
613 let mut new_headers = Vec::with_capacity(keep_indices.len() * HEADER_SIZE);
615 let mut new_data = Vec::with_capacity(keep_indices.len() * BLOCK_DATA_SIZE);
616 for &idx in &keep_indices {
617 let hdr_off = idx * HEADER_SIZE;
618 new_headers.extend_from_slice(&headers[hdr_off..hdr_off + HEADER_SIZE]);
619 if let Some((data_start, data_len)) = block_data_span(&headers, idx) {
621 let data_end = data_start.saturating_add(data_len).min(data.len());
622 if data_start < data_end {
623 new_data.extend_from_slice(&data[data_start..data_end]);
624 }
625 }
626 }
627 let hdr_tmp = output_dir.join("microscope.bin.tmp");
628 let dat_tmp = output_dir.join("data.bin.tmp");
629 fs::write(&hdr_tmp, &new_headers).map_err(|e| format!("write microscope.bin: {e}"))?;
630 fs::write(&dat_tmp, &new_data).map_err(|e| format!("write data.bin: {e}"))?;
631 fs::rename(&hdr_tmp, &hdr_path).map_err(|e| format!("rename microscope.bin: {e}"))?;
632 fs::rename(&dat_tmp, &dat_path).map_err(|e| format!("rename data.bin: {e}"))?;
633
634 let n = keep_indices.len();
636 let mut new_meta = Vec::with_capacity(META_HEADER_SIZE + 9 * DEPTH_ENTRY_SIZE);
637 if meta.len() >= 8 {
638 new_meta.extend_from_slice(&meta[..8]);
639 } else {
640 new_meta.extend_from_slice(b"MSC4\x04\0\0\0");
641 }
642 new_meta.extend_from_slice(&(n as u32).to_le_bytes());
643 let mut depth_counts = [0u32; 9];
644 for &idx in &keep_indices {
645 let off = idx * HEADER_SIZE;
646 let depth = headers[off + 14]; if (depth as usize) < 9 {
648 depth_counts[depth as usize] += 1;
649 }
650 }
651 let mut running_start = 0u32;
652 for &count in &depth_counts[..9] {
653 new_meta.extend_from_slice(&running_start.to_le_bytes());
654 new_meta.extend_from_slice(&count.to_le_bytes());
655 running_start += count;
656 }
657 let meta_tail_start = META_HEADER_SIZE + 9 * DEPTH_ENTRY_SIZE;
658 if meta_tail_start < meta.len() {
659 new_meta.extend_from_slice(&meta[meta_tail_start..]);
660 }
661 let meta_tmp = output_dir.join("meta.bin.tmp");
662 fs::write(&meta_tmp, &new_meta).map_err(|e| format!("write meta.bin: {e}"))?;
663 fs::rename(&meta_tmp, &meta_path).map_err(|e| format!("rename meta.bin: {e}"))?;
664
665 trim_embeddings(output_dir, &keep_indices)?;
666
667 Ok(target as u32)
668}
669
670fn trim_embeddings(output_dir: &Path, keep_indices: &[usize]) -> Result<(), String> {
674 let emb_path = output_dir.join("embeddings.bin");
675 if !emb_path.exists() {
676 return Ok(());
677 }
678 let emb = fs::read(&emb_path).map_err(|e| format!("read embeddings.bin: {e}"))?;
679 if emb.len() < 12 {
680 return Ok(());
681 }
682 let block_count = u32::from_le_bytes(emb[0..4].try_into().unwrap()) as usize;
683 let dim = u32::from_le_bytes(emb[4..8].try_into().unwrap()) as usize;
684 let expected = 12 + block_count * dim * 4;
685 if dim == 0 || block_count == 0 || emb.len() < expected {
686 return Ok(());
687 }
688 let mut out = Vec::with_capacity(12 + keep_indices.len() * dim * 4);
689 out.extend_from_slice(&(keep_indices.len() as u32).to_le_bytes());
690 out.extend_from_slice(&(dim as u32).to_le_bytes());
691 out.extend_from_slice(&emb[8..12]); for &idx in keep_indices {
693 let off = 12 + idx * dim * 4;
694 out.extend_from_slice(&emb[off..off + dim * 4]);
695 }
696 let tmp = output_dir.join("embeddings.bin.tmp");
697 fs::write(&tmp, &out).map_err(|e| format!("write embeddings.bin: {e}"))?;
698 fs::rename(&tmp, &emb_path).map_err(|e| format!("rename embeddings.bin: {e}"))?;
699 Ok(())
700}
701
702pub fn promote_recalled_blocks(
710 output_dir: &Path,
711 layers_dir: &Path,
712 block_count: usize,
713 promote_energy: f32,
714 protect_min_importance: u8,
715) -> Result<u32, String> {
716 use crate::HEADER_SIZE;
717
718 let hdr_path = output_dir.join("microscope.bin");
719 if !hdr_path.exists() {
720 return Ok(0);
721 }
722 let headers = fs::read(&hdr_path).map_err(|e| format!("read microscope.bin: {e}"))?;
723 let actual = headers.len() / HEADER_SIZE;
724 if actual == 0 || actual != block_count {
725 return Ok(0);
726 }
727 let hebb = crate::hebbian::HebbianState::load_or_init(output_dir, actual);
728
729 let mut bumps: Vec<(usize, u8)> = Vec::new(); let epistemic_gate = output_dir.exists();
733 let ledger = if epistemic_gate {
734 crate::epistemic::EvidenceLedger::load_or_init(output_dir)
735 } else {
736 crate::epistemic::EvidenceLedger {
737 records: std::collections::HashMap::new(),
738 }
739 };
740
741 for i in 0..actual {
742 let imp = headers[i * HEADER_SIZE + 48]; if imp >= protect_min_importance {
744 continue; }
746 let rec = match hebb.activations.get(i) {
747 Some(r) => r,
748 None => continue,
749 };
750 if rec.energy >= promote_energy && rec.activation_count > 0 {
751 let flags = headers[i * HEADER_SIZE + 49]; let class = crate::epistemic::class_of_flags(flags);
754 let block_hash = {
755 let data_path = output_dir.join("data.bin");
757 if let Ok(data) = fs::read(&data_path) {
758 if let Some((start, len)) = block_data_span(&headers, i) {
762 let end = start.saturating_add(len).min(data.len());
763 if start < end {
764 let block = &data[start..end];
765 crate::epistemic::content_hash(String::from_utf8_lossy(block).trim())
766 } else {
767 0
768 }
769 } else {
770 0
771 }
772 } else {
773 0
774 }
775 };
776 if let Err(_reason) = crate::epistemic::check_promotion_gate(&ledger, class, block_hash)
777 {
778 continue;
780 }
781 bumps.push((i, imp.saturating_add(1).min(protect_min_importance)));
782 }
783 }
784 if bumps.is_empty() {
785 return Ok(0);
786 }
787
788 let mut new_headers = headers.clone();
790 for &(idx, new_imp) in &bumps {
791 new_headers[idx * HEADER_SIZE + 48] = new_imp; }
793 let hdr_tmp = output_dir.join("microscope.bin.tmp");
794 fs::write(&hdr_tmp, &new_headers).map_err(|e| format!("write microscope.bin: {e}"))?;
795 fs::rename(&hdr_tmp, &hdr_path).map_err(|e| format!("rename microscope.bin: {e}"))?;
796
797 let dat_path = output_dir.join("data.bin");
799 let data = fs::read(&dat_path).map_err(|e| format!("read data.bin: {e}"))?;
800 let mut updated_files: std::collections::HashMap<std::path::PathBuf, String> =
801 std::collections::HashMap::new();
802 for &(idx, new_imp) in &bumps {
803 let Some((start, len)) = block_data_span(&headers, idx) else {
805 continue;
806 };
807 let end = start.saturating_add(len).min(data.len());
808 if start >= end {
809 continue;
810 }
811 let block = &data[start..end];
812 let block_text = String::from_utf8_lossy(block).trim().to_string();
813 if block_text.len() < 8 {
814 continue;
815 }
816 if let Ok(entries) = fs::read_dir(layers_dir) {
817 for file in entries.filter_map(|e| e.ok()) {
818 let path = file.path();
819 if path.extension().and_then(|e| e.to_str()) != Some("txt") {
820 continue;
821 }
822 let content = updated_files
823 .entry(path.clone())
824 .or_insert_with(|| fs::read_to_string(&path).unwrap_or_default());
825 if let Some(next) = bump_entry_by_text_hash(content, &block_text, new_imp) {
826 *content = next;
827 break;
828 }
829 }
830 }
831 }
832 for (path, content) in &updated_files {
833 let tmp = path.with_extension("txt.tmp");
834 fs::write(&tmp, content).map_err(|e| format!("write layer file: {e}"))?;
835 fs::rename(&tmp, path).map_err(|e| format!("rename layer file: {e}"))?;
836 }
837
838 Ok(bumps.len() as u32)
839}
840
841fn bump_entry_by_text_hash(content: &str, block_text: &str, new_imp: u8) -> Option<String> {
848 let needle_hash = crate::fingerprint::fnv1a_hash(block_text.trim().as_bytes());
849 let mut entries: Vec<String> = content.split("\n\n").map(|s| s.to_string()).collect();
850 for entry in &mut entries {
851 let (stripped, _imp) = crate::reader::parse_imp_marker(entry);
852 if crate::fingerprint::fnv1a_hash(stripped.trim().as_bytes()) == needle_hash
853 && entry.starts_with("(imp=")
854 {
855 if let Some(end) = entry.find(')') {
856 *entry = format!("(imp={}){}", new_imp, &entry[end + 1..]);
857 return Some(entries.join("\n\n"));
858 }
859 }
860 }
861 None
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867 use crate::archetype::ArchetypeState;
868 use crate::hebbian::{ActivationFingerprint, ActivationRecord, CoactivationPair};
869 use crate::predictive_cache::PredictiveCache;
870 use crate::resonance::ResonanceState;
871 use crate::thought_graph::ThoughtGraphState;
872 use std::collections::HashMap;
873
874 fn make_hebb(block_count: usize) -> HebbianState {
875 HebbianState {
876 activations: vec![ActivationRecord::default(); block_count],
877 coactivations: HashMap::new(),
878 fingerprints: Vec::new(),
879 }
880 }
881
882 #[test]
883 fn test_dream_log_roundtrip() {
884 let tmp = tempfile::tempdir().expect("tempdir");
885 let state = DreamState {
886 cycles: vec![
887 DreamCycle {
888 timestamp_ms: 1000,
889 duration_ms: 50,
890 replayed_fingerprints: 10,
891 strengthened_pairs: 3,
892 pruned_pairs: 5,
893 pruned_activations: 2,
894 consolidated_patterns: 1,
895 forgotten_blocks: 0,
896 energy_before: 10.5,
897 energy_after: 8.2,
898 },
899 DreamCycle {
900 timestamp_ms: 2000,
901 duration_ms: 30,
902 replayed_fingerprints: 8,
903 strengthened_pairs: 2,
904 pruned_pairs: 3,
905 pruned_activations: 1,
906 consolidated_patterns: 0,
907 forgotten_blocks: 0,
908 energy_before: 8.2,
909 energy_after: 7.0,
910 },
911 ],
912 last_dream_ms: 2000,
913 };
914 state.save(tmp.path()).unwrap();
915 let loaded = DreamState::load_or_init(tmp.path());
916 assert_eq!(loaded.cycles.len(), 2);
917 assert_eq!(loaded.last_dream_ms, 2000);
918 assert_eq!(loaded.cycles[0].replayed_fingerprints, 10);
919 assert_eq!(loaded.cycles[1].pruned_pairs, 3);
920 }
921
922 #[test]
923 fn eviction_respects_importance_protection() {
924 use crate::{BLOCK_DATA_SIZE, HEADER_SIZE};
925 use std::fs;
926
927 let tmp = tempfile::tempdir().expect("tempdir");
928 let dir = tmp.path();
929 let n = 10usize;
930 let mut headers = Vec::new();
931 let mut data = Vec::new();
932 for i in 0..n {
933 let mut h = vec![0u8; HEADER_SIZE];
934 h[17] = 1; h[48] = if i < 2 { 8 } else { 5 }; h[16] = (i % 9) as u8; headers.extend_from_slice(&h);
938 data.extend_from_slice(&[b'a'; BLOCK_DATA_SIZE]);
939 }
940 let mut meta = Vec::new();
941 meta.extend_from_slice(b"MSC4\x04\0\0\0");
942 meta.extend_from_slice(&(n as u32).to_le_bytes());
943 for _ in 0..9 {
944 meta.extend_from_slice(&0u32.to_le_bytes());
945 meta.extend_from_slice(&0u32.to_le_bytes());
946 }
947 fs::write(dir.join("microscope.bin"), &headers).unwrap();
948 fs::write(dir.join("data.bin"), &data).unwrap();
949 fs::write(dir.join("meta.bin"), &meta).unwrap();
950
951 let evicted = evict_over_capacity(dir, 4, 8).unwrap();
952 assert_eq!(evicted, 6);
953
954 let new_headers = fs::read(dir.join("microscope.bin")).unwrap();
955 assert_eq!(new_headers.len() / HEADER_SIZE, 4);
956 let mut protected_survivors = 0;
957 let mut min_imp = u8::MAX;
958 for i in 0..4 {
959 let imp = new_headers[i * HEADER_SIZE + 48]; if imp >= 8 {
961 protected_survivors += 1;
962 }
963 min_imp = min_imp.min(imp);
964 }
965 assert_eq!(protected_survivors, 2, "both protected blocks survive");
966 assert!(min_imp >= 5, "no protected block is evicted");
967 }
968
969 #[test]
970 fn eviction_is_noop_below_cap_or_without_candidates() {
971 use crate::{BLOCK_DATA_SIZE, HEADER_SIZE};
972 use std::fs;
973
974 let tmp = tempfile::tempdir().expect("tempdir");
975 let dir = tmp.path();
976 let n = 6usize;
977 let mut headers = Vec::new();
978 let mut data = Vec::new();
979 for _ in 0..n {
980 let mut h = vec![0u8; HEADER_SIZE];
981 h[48] = 8; headers.extend_from_slice(&h);
983 data.extend_from_slice(&[b'a'; BLOCK_DATA_SIZE]);
984 }
985 fs::write(dir.join("microscope.bin"), &headers).unwrap();
986 fs::write(dir.join("data.bin"), &data).unwrap();
987 fs::write(dir.join("meta.bin"), b"MSC4\x04\0\0\0").unwrap();
988
989 assert_eq!(evict_over_capacity(dir, 100, 8).unwrap(), 0);
991 assert_eq!(evict_over_capacity(dir, 2, 8).unwrap(), 0);
993 let after = fs::read(dir.join("microscope.bin")).unwrap();
994 assert_eq!(after.len() / HEADER_SIZE, n);
995 }
996
997 #[test]
998 fn promotion_bumps_recalled_blocks_and_mirrors_layer_source() {
999 use crate::hebbian::{ActivationRecord, HebbianState};
1000 use crate::{BLOCK_DATA_SIZE, HEADER_SIZE};
1001 use std::fs;
1002
1003 let tmp = tempfile::tempdir().expect("tempdir");
1004 let dir = tmp.path();
1005 let layer_dir = dir.join("layers");
1006 fs::create_dir_all(&layer_dir).unwrap();
1007 let n = 4usize;
1008
1009 let mut headers = Vec::new();
1010 let mut data = Vec::new();
1011 for i in 0..n {
1018 let text = format!("emlék szöveg blokk {}", i);
1019 let bytes = text.as_bytes();
1020 let mut h = vec![0u8; HEADER_SIZE];
1021 h[17] = 0; h[48] = 5; h[18..22].copy_from_slice(&(data.len() as u32).to_le_bytes());
1024 h[22..24].copy_from_slice(&(bytes.len() as u16).to_le_bytes());
1025 headers.extend_from_slice(&h);
1026 let mut block = bytes.to_vec();
1032 block.resize(BLOCK_DATA_SIZE, 0);
1033 data.extend_from_slice(&block);
1034 }
1035 fs::write(dir.join("microscope.bin"), &headers).unwrap();
1036 fs::write(dir.join("data.bin"), &data).unwrap();
1037 fs::write(dir.join("meta.bin"), b"MSC4\x04\0\0\0").unwrap();
1038 fs::write(
1039 layer_dir.join("long_term.txt"),
1040 "(imp=5) emlék szöveg blokk 0\n\n(imp=5) emlék szöveg blokk 1\n\n\
1041 (imp=5) emlék szöveg blokk 2\n\n(imp=5) emlék szöveg blokk 3",
1042 )
1043 .unwrap();
1044
1045 let mut hebb = HebbianState {
1046 activations: vec![ActivationRecord::default(); n],
1047 coactivations: HashMap::new(),
1048 fingerprints: Vec::new(),
1049 };
1050 hebb.activations[1].energy = 0.8;
1051 hebb.activations[1].activation_count = 3;
1052 hebb.activations[2].energy = 0.9; hebb.save(dir).unwrap();
1054
1055 let promoted = promote_recalled_blocks(dir, &layer_dir, n, 0.35, 8).unwrap();
1056 assert_eq!(promoted, 1);
1057
1058 let new_headers = fs::read(dir.join("microscope.bin")).unwrap();
1059 assert_eq!(new_headers[HEADER_SIZE + 48], 6);
1060 assert_eq!(
1061 new_headers[2 * HEADER_SIZE + 48],
1062 5,
1063 "no activation -> no promotion"
1064 );
1065 assert_eq!(new_headers[48], 5);
1066
1067 let layer = fs::read_to_string(layer_dir.join("long_term.txt")).unwrap();
1068 assert!(
1069 layer.contains("(imp=6) emlék szöveg blokk 1"),
1070 "layer source must mirror the promotion"
1071 );
1072 assert!(layer.contains("(imp=5) emlék szöveg blokk 2"));
1073 }
1074
1075 #[test]
1076 fn bump_entry_matches_exact_text_not_substring() {
1077 let content = "(imp=5) fontos dolog\n\n(imp=5) ez fontos dolog";
1078 let result = bump_entry_by_text_hash(content, "fontos dolog", 7).unwrap();
1079 assert!(result.starts_with("(imp=7) fontos dolog"));
1080 assert!(
1081 result.contains("\n\n(imp=5) ez fontos dolog"),
1082 "overlapping entry must stay untouched"
1083 );
1084 }
1085
1086 #[test]
1087 fn bump_entry_changes_at_most_one_entry() {
1088 let content = "(imp=5) ugyanaz\n\n(imp=5) ugyanaz";
1089 let result = bump_entry_by_text_hash(content, "ugyanaz", 7).unwrap();
1090 assert_eq!(result.matches("(imp=7) ugyanaz").count(), 1);
1091 assert_eq!(result.matches("(imp=5) ugyanaz").count(), 1);
1092 }
1093
1094 #[test]
1095 fn test_dream_strengthens_repeated_coactivations() {
1096 let tmp = tempfile::tempdir().expect("tempdir");
1097 let mut hebb = make_hebb(10);
1098
1099 hebb.coactivations.insert(
1101 (0, 1),
1102 CoactivationPair {
1103 block_a: 0,
1104 block_b: 1,
1105 count: 5,
1106 last_ts_ms: now_ms(),
1107 },
1108 );
1109
1110 let now = now_ms();
1112 for i in 0..3 {
1113 hebb.fingerprints.push(ActivationFingerprint {
1114 timestamp_ms: now - i * 1000,
1115 query_hash: 100 + i,
1116 activations: vec![(0, 0.5), (1, 0.3)],
1117 });
1118 }
1119
1120 hebb.save(tmp.path()).unwrap();
1121
1122 let tg = ThoughtGraphState::load_or_init(tmp.path());
1124 tg.save(tmp.path()).unwrap();
1125 let pc = PredictiveCache::load_or_init(tmp.path());
1126 pc.save(tmp.path()).unwrap();
1127 let res = ResonanceState::load_or_init(tmp.path());
1128 res.save(tmp.path()).unwrap();
1129 let arc = ArchetypeState::load_or_init(tmp.path());
1130 arc.save(tmp.path()).unwrap();
1131
1132 let cycle = dream_consolidate(tmp.path(), 10, 0, 8).unwrap();
1133 assert!(cycle.strengthened_pairs > 0);
1134
1135 let hebb2 = HebbianState::load_or_init(tmp.path(), 10);
1137 let pair = hebb2.coactivations.get(&(0, 1)).unwrap();
1138 assert!(pair.count > 5); }
1140
1141 #[test]
1142 fn test_dream_prunes_weak_pairs() {
1143 let tmp = tempfile::tempdir().expect("tempdir");
1144 let mut hebb = make_hebb(5);
1145
1146 hebb.coactivations.insert(
1148 (0, 1),
1149 CoactivationPair {
1150 block_a: 0,
1151 block_b: 1,
1152 count: 1,
1153 last_ts_ms: 1000, },
1155 );
1156 hebb.coactivations.insert(
1158 (2, 3),
1159 CoactivationPair {
1160 block_a: 2,
1161 block_b: 3,
1162 count: 10,
1163 last_ts_ms: now_ms(),
1164 },
1165 );
1166
1167 hebb.save(tmp.path()).unwrap();
1168 ThoughtGraphState::load_or_init(tmp.path())
1169 .save(tmp.path())
1170 .unwrap();
1171 PredictiveCache::load_or_init(tmp.path())
1172 .save(tmp.path())
1173 .unwrap();
1174 ResonanceState::load_or_init(tmp.path())
1175 .save(tmp.path())
1176 .unwrap();
1177 ArchetypeState::load_or_init(tmp.path())
1178 .save(tmp.path())
1179 .unwrap();
1180
1181 let cycle = dream_consolidate(tmp.path(), 5, 0, 8).unwrap();
1182 assert_eq!(cycle.pruned_pairs, 1);
1183
1184 let hebb2 = HebbianState::load_or_init(tmp.path(), 5);
1185 assert!(!hebb2.coactivations.contains_key(&(0, 1))); assert!(hebb2.coactivations.contains_key(&(2, 3))); }
1188
1189 #[test]
1190 fn test_dream_replays_fingerprints() {
1191 let tmp = tempfile::tempdir().expect("tempdir");
1192 let mut hebb = make_hebb(5);
1193
1194 assert_eq!(hebb.activations[0].energy, 0.0);
1196
1197 hebb.fingerprints.push(ActivationFingerprint {
1199 timestamp_ms: now_ms() - 1000,
1200 query_hash: 42,
1201 activations: vec![(0, 0.5)],
1202 });
1203
1204 hebb.save(tmp.path()).unwrap();
1205 ThoughtGraphState::load_or_init(tmp.path())
1206 .save(tmp.path())
1207 .unwrap();
1208 PredictiveCache::load_or_init(tmp.path())
1209 .save(tmp.path())
1210 .unwrap();
1211 ResonanceState::load_or_init(tmp.path())
1212 .save(tmp.path())
1213 .unwrap();
1214 ArchetypeState::load_or_init(tmp.path())
1215 .save(tmp.path())
1216 .unwrap();
1217
1218 let cycle = dream_consolidate(tmp.path(), 5, 0, 8).unwrap();
1219 assert_eq!(cycle.replayed_fingerprints, 1);
1220
1221 let hebb2 = HebbianState::load_or_init(tmp.path(), 5);
1222 assert!(hebb2.activations[0].energy >= REPLAY_ENERGY - 0.01);
1223 }
1224
1225 #[test]
1226 fn test_dream_no_fingerprints() {
1227 let tmp = tempfile::tempdir().expect("tempdir");
1228 let hebb = make_hebb(5);
1229 hebb.save(tmp.path()).unwrap();
1230 ThoughtGraphState::load_or_init(tmp.path())
1231 .save(tmp.path())
1232 .unwrap();
1233 PredictiveCache::load_or_init(tmp.path())
1234 .save(tmp.path())
1235 .unwrap();
1236 ResonanceState::load_or_init(tmp.path())
1237 .save(tmp.path())
1238 .unwrap();
1239 ArchetypeState::load_or_init(tmp.path())
1240 .save(tmp.path())
1241 .unwrap();
1242
1243 let cycle = dream_consolidate(tmp.path(), 5, 0, 8).unwrap();
1244 assert_eq!(cycle.replayed_fingerprints, 0);
1245 assert_eq!(cycle.strengthened_pairs, 0);
1246 assert_eq!(cycle.pruned_pairs, 0);
1247 }
1248
1249 #[test]
1250 fn test_dream_stats() {
1251 let state = DreamState {
1252 cycles: vec![
1253 DreamCycle {
1254 timestamp_ms: 1000,
1255 duration_ms: 50,
1256 replayed_fingerprints: 10,
1257 strengthened_pairs: 3,
1258 pruned_pairs: 5,
1259 pruned_activations: 2,
1260 consolidated_patterns: 1,
1261 forgotten_blocks: 0,
1262 energy_before: 10.0,
1263 energy_after: 8.0,
1264 },
1265 DreamCycle {
1266 timestamp_ms: 2000,
1267 duration_ms: 30,
1268 replayed_fingerprints: 8,
1269 strengthened_pairs: 2,
1270 pruned_pairs: 3,
1271 pruned_activations: 1,
1272 consolidated_patterns: 0,
1273 forgotten_blocks: 0,
1274 energy_before: 8.0,
1275 energy_after: 7.0,
1276 },
1277 ],
1278 last_dream_ms: 2000,
1279 };
1280 let stats = state.stats();
1281 assert_eq!(stats.total_cycles, 2);
1282 assert_eq!(stats.total_pruned_pairs, 8);
1283 assert_eq!(stats.total_strengthened, 5);
1284 assert_eq!(stats.total_replayed, 18);
1285 }
1286}