Skip to main content

microscope_memory/
reconsolidation.rs

1//! Rekonszolidáció — minden recall ÁTÍRJA a memóriát.
2//!
3//! Az emberi agyban minden felidézés rekonszolidáció: a memória az aktuális
4//! kontextusba ágyazva íródik újra. Itt ugyanez:
5//! - Emotion blend: a blokk érzelmi vektora közelít a query emotion-hoz
6//! - Spatial drift: a blokk koordinátái húzódnak a query térhez
7//!
8//! Ez teszi a rendszert PATHDEPENDENS-sé: két azonos store → különböző
9//! recall útvonalak → különböző memóriaállapotok. Mint egy igzi agy.
10
11use std::path::Path;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use crate::reader::{emotional_similarity, load_emotion_lookup};
15use crate::{content_coords_blended, MicroscopeReader};
16
17// ─── Constants ──────────────────────────────────────
18
19/// Emotion blend rate: mennyit mozdul a blokk emotion vektora a query felé.
20const EMOTION_BLEND_RATE: f32 = 0.15;
21/// Spatial drift rate: mennyit mozdul a blokk koordinátája a query felé.
22const SPATIAL_DRIFT_RATE: f32 = 0.02;
23/// Ha a query emotion és a blokk emotion hasonlósága e felett van, nem blend-elünk (már hasonló).
24const MAX_EMO_SIM_FOR_RECONSOLIDATION: f32 = 0.85;
25/// Minimum recall quality a reconsolidation triggeréhez.
26const MIN_QUALITY: u8 = 3;
27/// Ha ennyi idő (ms) óta nem volt aktiválva a blokk, nagyobb a drift.
28const STALE_THRESHOLD_MS: u64 = 86_400_000 * 3; // 3 days
29
30// ─── Reconsolidation ───────────────────────────────
31
32/// Reconsolidate emotion vectors of recalled blocks.
33/// Minden activated blokk emotion vektorát blend-eli a query emotion-nal,
34/// ha a hasonlóság még nem túl magas.
35pub fn reconsolidate_emotions(
36    output_dir: &Path,
37    reader: &MicroscopeReader,
38    _query: &str,
39    query_emotion: Option<&[f32; 21]>,
40    activated: &[(u32, f32)],
41) -> u32 {
42    let qe = match query_emotion {
43        Some(e) => e,
44        None => {
45            // Ha nincs explicit query emotion, próbáljuk az EmotionalStateRing-et
46            let ring = crate::emotional_state::EmotionalStateRing::load_or_init(output_dir);
47            if ring.is_active() {
48                // Use a small allocation to hold the reference... just use the ring.current
49                let current: [f32; 21] = ring.current;
50                return reconsolidate_emotions_inner(output_dir, reader, &current, activated);
51            }
52            return 0;
53        }
54    };
55    reconsolidate_emotions_inner(output_dir, reader, qe, activated)
56}
57
58fn reconsolidate_emotions_inner(
59    output_dir: &Path,
60    reader: &MicroscopeReader,
61    query_emotion: &[f32; 21],
62    activated: &[(u32, f32)],
63) -> u32 {
64    let lookup = match load_emotion_lookup(output_dir) {
65        Some(l) => l,
66        None => return 0,
67    };
68    let emotions_path = output_dir.join("emotions.bin");
69    let mut blended = 0u32;
70    let mut updates: Vec<(usize, [f32; 21])> = Vec::new();
71
72    // Csak a main index blokkokat reconsolidáljuk (append log entry-ket nem)
73    let block_count = reader.block_count;
74
75    for &(idx, _) in activated {
76        let idx_usize = idx as usize;
77        if idx_usize >= block_count {
78            continue;
79        }
80
81        // Olvasd ki a jelenlegi emotion vektort
82        let current_emo = lookup(idx_usize).unwrap_or([0.0f32; 21]);
83
84        // Ha már nagyon hasonló, hagyd
85        let sim = emotional_similarity(query_emotion, &current_emo);
86        if sim > MAX_EMO_SIM_FOR_RECONSOLIDATION {
87            continue;
88        }
89
90        // Blend: current * (1 - rate) + query * rate
91        let rate = EMOTION_BLEND_RATE * (1.0 - sim); // minél kevésbé hasonló, annál többet blend-el
92        let mut new_emo = [0.0f32; 21];
93        for i in 0..21 {
94            new_emo[i] = current_emo[i] * (1.0 - rate) + query_emotion[i] * rate;
95        }
96
97        // Normalizáljuk: max érték ne haladja meg az 1.0-t
98        let max_val = new_emo.iter().cloned().fold(0.0f32, f32::max);
99        if max_val > 1.0 {
100            for v in &mut new_emo {
101                *v /= max_val;
102            }
103        }
104
105        // Collect and write once. `write_emotion` does the whole file per call
106        // -- read 58.7 MB, patch 84 bytes, write 58.7 MB, rename -- so calling
107        // it once per activated block moved ~1.2 GB per recall. That measured
108        // as 4.2 s of an MCP recall, on every call that primes an emotion from
109        // the state ring.
110        updates.push((idx_usize, new_emo));
111    }
112
113    if !updates.is_empty() {
114        if let Err(e) = crate::reader::write_emotions_batch(&emotions_path, &updates) {
115            eprintln!("  WARN batch emotion write: {}", e);
116            return blended;
117        }
118        blended += updates.len() as u32;
119    }
120
121    blended
122}
123
124/// Spatial reconsolidation: drift blocks toward query coordinates.
125/// Minden activated blokk koordinátáit húzza a query térbeli pozíciója felé,
126/// a HebbianState drift mezőin keresztül (a drift a következő build-nél
127/// beégetődik a header-ekbe).
128pub fn reconsolidate_spatial(
129    output_dir: &Path,
130    reader: &MicroscopeReader,
131    query: &str,
132    config: &crate::config::Config,
133    activated: &[(u32, f32)],
134) -> u32 {
135    let sw = config.search.semantic_weight;
136    let (qx, qy, qz) = content_coords_blended(query, "long_term", sw);
137    let block_count = reader.block_count;
138
139    // Load Hebbian state
140    let mut hebb = crate::hebbian::HebbianState::load_or_init(output_dir, block_count);
141    let now_ms = SystemTime::now()
142        .duration_since(UNIX_EPOCH)
143        .unwrap_or_default()
144        .as_millis() as u64;
145
146    let mut drifted = 0u32;
147
148    for &(idx, _) in activated {
149        let idx_usize = idx as usize;
150        if idx_usize >= block_count {
151            continue;
152        }
153
154        let hdr = reader.header(idx_usize);
155        let dx = qx - hdr.x;
156        let dy = qy - hdr.y;
157        let dz = qz - hdr.z;
158        let dist = (dx * dx + dy * dy + dz * dz).sqrt();
159
160        // Ha túl közel van, nem kell drift
161        if dist < 0.001 {
162            continue;
163        }
164
165        // Minél távolabbi, annál nagyobb a drift
166        let recency_factor = if idx_usize < hebb.activations.len() {
167            let age = now_ms.saturating_sub(hebb.activations[idx_usize].last_activated_ms);
168            if age > STALE_THRESHOLD_MS {
169                2.0 // régi memória → nagyobb drift
170            } else {
171                1.0
172            }
173        } else {
174            1.0
175        };
176
177        let rate = SPATIAL_DRIFT_RATE * dist * recency_factor;
178        let rate = rate.min(0.1); // max 10% egy alkalommal (nem akarjuk destabilizálni)
179
180        if idx_usize < hebb.activations.len() {
181            hebb.activations[idx_usize].drift_x += dx * rate;
182            hebb.activations[idx_usize].drift_y += dy * rate;
183            hebb.activations[idx_usize].drift_z += dz * rate;
184            drifted += 1;
185        }
186    }
187
188    if drifted > 0 {
189        let _ = hebb.save(output_dir);
190    }
191
192    drifted
193}
194
195/// Full reconsolidation: emotion + spatial, minden activated blokkra.
196/// Visszaadja a rekonszolidált blokkok számát (emotion + spatial együtt).
197pub fn reconsolidate(
198    output_dir: &Path,
199    reader: &MicroscopeReader,
200    query: &str,
201    query_emotion: Option<&[f32; 21]>,
202    config: &crate::config::Config,
203    quality: u8,
204    activated: &[(u32, f32)],
205) -> (u32, u32) {
206    if quality < MIN_QUALITY || activated.is_empty() {
207        return (0, 0);
208    }
209
210    let emo_count = reconsolidate_emotions(output_dir, reader, query, query_emotion, activated);
211    let spatial_count = reconsolidate_spatial(output_dir, reader, query, config, activated);
212
213    (emo_count, spatial_count)
214}
215
216/// Formázás CLI kimenethez (színek nélkül, main.rs-ben lesz színezve).
217pub fn format_reconsolidation(emo: u32, spatial: u32) -> String {
218    if emo == 0 && spatial == 0 {
219        String::new()
220    } else {
221        format!("  RECONSOLIDATED emotion={} spatial={}", emo, spatial)
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_emotion_blend_basic() {
231        let qe = [
232            0.9f32, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
233            0.0, 0.0, 0.0, 0.0,
234        ];
235        let ce = [
236            0.1f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.8, 0.0, 0.0, 0.0,
237            0.0, 0.0, 0.0, 0.0,
238        ]; // anxiety=0.8
239        let sim = emotional_similarity(&qe, &ce);
240        assert!(sim < MAX_EMO_SIM_FOR_RECONSOLIDATION); // joy vs anxiety → low sim
241
242        let rate = EMOTION_BLEND_RATE * (1.0 - sim);
243        let mut blended = [0.0f32; 21];
244        for i in 0..21 {
245            blended[i] = ce[i] * (1.0 - rate) + qe[i] * rate;
246        }
247        // blended[0] should be > 0.1 (joy blended in)
248        assert!(blended[0] > 0.1);
249        // blended[13] (anxiety) should be < 0.8 (diluted)
250        assert!(blended[13] < ce[13]);
251    }
252
253    #[test]
254    fn test_high_similarity_skips_blend() {
255        let qe = [0.8f32; 21];
256        let ce = [0.78f32; 21];
257        let sim = emotional_similarity(&qe, &ce);
258        assert!(sim > MAX_EMO_SIM_FOR_RECONSOLIDATION); // should be very close
259    }
260}