Skip to main content

microscope_memory/
attention.rs

1//! Attention Mechanism — dynamic weighting of consciousness layers.
2//!
3//! Computes an attention vector over 7 layers based on query context:
4//! query length, emotional energy, session depth, pattern confidence,
5//! cache hit rate, archetype match score.
6//!
7//! Tracks which attention distributions lead to good results (user stops
8//! searching) vs bad results (user immediately re-queries). Over time,
9//! learns which layer weights work best.
10//!
11//! Binary format: attention.bin (ATT1)
12
13use std::fs;
14use std::io::Write;
15use std::path::Path;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18// ─── Constants ──────────────────────────────────────
19
20/// Layer indices: Hebbian, Mirror, Resonance, Archetype, Emotional, ThoughtGraph, PredictiveCache
21pub const NUM_LAYERS: usize = 7;
22pub const LAYER_NAMES: [&str; NUM_LAYERS] = [
23    "Hebbian",
24    "Mirror",
25    "Resonance",
26    "Archetype",
27    "Emotional",
28    "ThoughtGraph",
29    "PredictiveCache",
30];
31
32const MAX_HISTORY: usize = 200;
33const LEARN_RATE: f32 = 0.05;
34const QUALITY_GOOD_THRESHOLD: f32 = 0.7;
35const QUALITY_BAD_THRESHOLD: f32 = 0.3;
36
37/// Time thresholds for quality inference (ms).
38const SATISFIED_MS: u64 = 60_000; // >60s gap = satisfied
39const UNSATISFIED_MS: u64 = 5_000; // <5s gap = unsatisfied
40
41// ─── AttentionSignals ───────────────────────────────
42
43/// Input signals for computing attention weights.
44pub struct AttentionSignals {
45    pub query_length: usize,
46    pub emotional_energy: f32,
47    /// Magnitude of the current 21D emotional state ring (0.0 = neutral).
48    pub emotional_intensity: f32,
49    pub session_depth: usize,
50    pub pattern_confidence: f32,
51    pub cache_hit_rate: f32,
52    pub archetype_match_score: f32,
53}
54
55// ─── AttentionVector ────────────────────────────────
56
57/// Computed attention weights for a single recall.
58#[derive(Clone, Debug)]
59pub struct AttentionVector {
60    pub weights: [f32; NUM_LAYERS],
61}
62
63impl AttentionVector {
64    /// Get weight for a specific layer.
65    pub fn weight(&self, layer: usize) -> f32 {
66        if layer < NUM_LAYERS {
67            self.weights[layer]
68        } else {
69            1.0
70        }
71    }
72}
73
74// ─── AttentionOutcome ───────────────────────────────
75
76/// Recorded outcome for learning.
77#[derive(Clone, Debug)]
78pub struct AttentionOutcome {
79    pub weights: [f32; NUM_LAYERS],
80    pub timestamp_ms: u64,
81    pub quality: f32,
82}
83
84const OUTCOME_BYTES: usize = NUM_LAYERS * 4 + 8 + 4; // 28 + 8 + 4 = 40
85
86// ─── AttentionState ─────────────────────────────────
87
88#[derive(Clone)]
89pub struct AttentionState {
90    /// Learned optimal weights (running average from good outcomes).
91    pub learned_weights: [f32; NUM_LAYERS],
92    /// Recent outcome history.
93    pub history: Vec<AttentionOutcome>,
94    /// Last recall timestamp for quality inference.
95    pub last_recall_ms: u64,
96    /// Total recalls tracked.
97    pub total_recalls: u32,
98}
99
100impl AttentionState {
101    pub fn load_or_init(output_dir: &Path) -> Self {
102        let path = output_dir.join("attention.bin");
103        if path.exists() {
104            load_attention(&path)
105        } else {
106            Self {
107                learned_weights: [1.0; NUM_LAYERS],
108                history: Vec::new(),
109                last_recall_ms: 0,
110                total_recalls: 0,
111            }
112        }
113    }
114
115    /// Compute attention vector from context signals.
116    pub fn compute_attention(&self, signals: &AttentionSignals) -> AttentionVector {
117        let mut raw = [1.0f32; NUM_LAYERS];
118
119        // Factor 1: query length
120        if signals.query_length <= 10 {
121            raw[0] *= 1.5; // Hebbian: short = familiar territory
122            raw[3] *= 1.3; // Archetype: short = concept lookup
123        } else {
124            raw[5] *= 1.3; // ThoughtGraph: long = complex reasoning chain
125        }
126
127        // Factor 2: emotional energy + intensity
128        raw[4] *= 1.0 + signals.emotional_energy.min(2.0);
129        raw[4] *= 1.0 + signals.emotional_intensity.min(2.0);
130
131        // Factor 3: session depth
132        let depth_factor = (signals.session_depth as f32 / 5.0).min(1.0);
133        raw[5] *= 1.0 + depth_factor * 0.5; // ThoughtGraph benefits from deep sessions
134        raw[6] *= 1.0 + depth_factor * 0.3; // PredictiveCache needs session context
135
136        // Factor 4: pattern confidence
137        raw[5] *= 1.0 + signals.pattern_confidence;
138
139        // Factor 5: cache hit rate
140        raw[6] *= 1.0 + signals.cache_hit_rate;
141
142        // Factor 6: archetype match
143        raw[3] *= 1.0 + signals.archetype_match_score.min(2.0);
144
145        // Blend with learned weights (80% computed, 20% learned)
146        for (i, w) in raw.iter_mut().enumerate() {
147            *w = *w * 0.8 + self.learned_weights[i] * 0.2;
148        }
149
150        // Normalize so average weight = 1.0
151        let sum: f32 = raw.iter().sum();
152        if sum > 0.0 && sum.is_finite() {
153            let scale = NUM_LAYERS as f32 / sum;
154            for w in &mut raw {
155                *w *= scale;
156            }
157        }
158
159        AttentionVector { weights: raw }
160    }
161
162    /// Infer quality of last recall from time gap.
163    /// Returns quality score (0.0 = bad, 1.0 = good).
164    pub fn infer_quality(&self) -> f32 {
165        if self.last_recall_ms == 0 {
166            return 0.5; // no data
167        }
168        let now = now_epoch_ms();
169        let gap = now.saturating_sub(self.last_recall_ms);
170
171        if gap >= SATISFIED_MS {
172            1.0
173        } else if gap <= UNSATISFIED_MS {
174            0.2
175        } else {
176            // Linear interpolation
177            let t = (gap - UNSATISFIED_MS) as f32 / (SATISFIED_MS - UNSATISFIED_MS) as f32;
178            0.2 + t * 0.8
179        }
180    }
181
182    /// Record the outcome of a recall (the quality applies to the PREVIOUS recall).
183    pub fn record_outcome(&mut self, quality: f32, weights: &[f32; NUM_LAYERS]) {
184        self.history.push(AttentionOutcome {
185            weights: *weights,
186            timestamp_ms: now_epoch_ms(),
187            quality,
188        });
189
190        if self.history.len() > MAX_HISTORY {
191            self.history.drain(0..(self.history.len() - MAX_HISTORY));
192        }
193
194        self.update_learned_weights();
195    }
196
197    /// Mark that a recall just happened (for next quality inference).
198    pub fn mark_recall(&mut self) {
199        self.last_recall_ms = now_epoch_ms();
200        self.total_recalls += 1;
201    }
202
203    /// Update learned weights from outcome history using EMA.
204    fn update_learned_weights(&mut self) {
205        let good: Vec<&AttentionOutcome> = self
206            .history
207            .iter()
208            .filter(|o| o.quality >= QUALITY_GOOD_THRESHOLD)
209            .collect();
210        let bad: Vec<&AttentionOutcome> = self
211            .history
212            .iter()
213            .filter(|o| o.quality <= QUALITY_BAD_THRESHOLD)
214            .collect();
215
216        if good.is_empty() && bad.is_empty() {
217            return;
218        }
219
220        for i in 0..NUM_LAYERS {
221            let good_avg = if good.is_empty() {
222                self.learned_weights[i]
223            } else {
224                good.iter().map(|o| o.weights[i]).sum::<f32>() / good.len() as f32
225            };
226            let bad_avg = if bad.is_empty() {
227                self.learned_weights[i]
228            } else {
229                bad.iter().map(|o| o.weights[i]).sum::<f32>() / bad.len() as f32
230            };
231
232            let delta = good_avg - bad_avg;
233            self.learned_weights[i] += delta * LEARN_RATE;
234            self.learned_weights[i] = self.learned_weights[i].clamp(0.1, 3.0);
235        }
236    }
237
238    /// Save to binary.
239    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
240        save_attention(&output_dir.join("attention.bin"), self)
241    }
242}
243
244// ─── Binary I/O ─────────────────────────────────────
245
246fn now_epoch_ms() -> u64 {
247    SystemTime::now()
248        .duration_since(UNIX_EPOCH)
249        .unwrap_or_default()
250        .as_millis() as u64
251}
252
253fn save_attention(path: &Path, state: &AttentionState) -> Result<(), String> {
254    let mut buf = Vec::with_capacity(48 + state.history.len() * OUTCOME_BYTES);
255
256    // Header
257    buf.write_all(b"ATT1").map_err(|e| e.to_string())?;
258    buf.write_all(&state.total_recalls.to_le_bytes())
259        .map_err(|e| e.to_string())?;
260    buf.write_all(&state.last_recall_ms.to_le_bytes())
261        .map_err(|e| e.to_string())?;
262
263    // Learned weights
264    for &w in &state.learned_weights {
265        buf.write_all(&w.to_le_bytes()).map_err(|e| e.to_string())?;
266    }
267
268    // History
269    buf.write_all(&(state.history.len() as u32).to_le_bytes())
270        .map_err(|e| e.to_string())?;
271
272    for outcome in &state.history {
273        for &w in &outcome.weights {
274            buf.write_all(&w.to_le_bytes()).map_err(|e| e.to_string())?;
275        }
276        buf.write_all(&outcome.timestamp_ms.to_le_bytes())
277            .map_err(|e| e.to_string())?;
278        buf.write_all(&outcome.quality.to_le_bytes())
279            .map_err(|e| e.to_string())?;
280    }
281
282    let tmp_path = path.with_extension("bin.tmp");
283    fs::write(&tmp_path, &buf).map_err(|e| e.to_string())?;
284    fs::rename(&tmp_path, path).map_err(|e| e.to_string())
285}
286
287fn load_attention(path: &Path) -> AttentionState {
288    let data = match fs::read(path) {
289        Ok(d) => d,
290        Err(_) => {
291            return AttentionState {
292                learned_weights: [1.0; NUM_LAYERS],
293                history: Vec::new(),
294                last_recall_ms: 0,
295                total_recalls: 0,
296            }
297        }
298    };
299
300    // Header: 4 (magic) + 4 (total_recalls) + 8 (last_recall_ms) + 28 (learned_weights) + 4 (history_count) = 48
301    if data.len() < 48 || &data[0..4] != b"ATT1" {
302        return AttentionState {
303            learned_weights: [1.0; NUM_LAYERS],
304            history: Vec::new(),
305            last_recall_ms: 0,
306            total_recalls: 0,
307        };
308    }
309
310    let total_recalls = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
311    let last_recall_ms = u64::from_le_bytes([
312        data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
313    ]);
314
315    let mut learned_weights = [0.0f32; NUM_LAYERS];
316    let mut offset = 16;
317    for w in &mut learned_weights {
318        *w = f32::from_le_bytes([
319            data[offset],
320            data[offset + 1],
321            data[offset + 2],
322            data[offset + 3],
323        ]);
324        // Sanitize: replace NaN/inf with 1.0 to prevent propagation
325        if w.is_nan() || w.is_infinite() {
326            *w = 1.0;
327        }
328        offset += 4;
329    }
330
331    let history_count = u32::from_le_bytes([
332        data[offset],
333        data[offset + 1],
334        data[offset + 2],
335        data[offset + 3],
336    ]) as usize;
337    offset += 4;
338
339    let mut history = Vec::with_capacity(history_count);
340    for _ in 0..history_count {
341        if offset + OUTCOME_BYTES > data.len() {
342            break;
343        }
344
345        let mut weights = [0.0f32; NUM_LAYERS];
346        for w in &mut weights {
347            *w = f32::from_le_bytes([
348                data[offset],
349                data[offset + 1],
350                data[offset + 2],
351                data[offset + 3],
352            ]);
353            if w.is_nan() || w.is_infinite() {
354                *w = 1.0;
355            }
356            offset += 4;
357        }
358
359        let timestamp_ms = u64::from_le_bytes([
360            data[offset],
361            data[offset + 1],
362            data[offset + 2],
363            data[offset + 3],
364            data[offset + 4],
365            data[offset + 5],
366            data[offset + 6],
367            data[offset + 7],
368        ]);
369        offset += 8;
370
371        let quality = f32::from_le_bytes([
372            data[offset],
373            data[offset + 1],
374            data[offset + 2],
375            data[offset + 3],
376        ]);
377        let quality = if quality.is_nan() || quality.is_infinite() {
378            0.5
379        } else {
380            quality
381        };
382        offset += 4;
383
384        history.push(AttentionOutcome {
385            weights,
386            timestamp_ms,
387            quality,
388        });
389    }
390
391    AttentionState {
392        learned_weights,
393        history,
394        last_recall_ms,
395        total_recalls,
396    }
397}
398
399// ─── Tests ──────────────────────────────────────────
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    fn default_signals() -> AttentionSignals {
406        AttentionSignals {
407            query_length: 20,
408            emotional_energy: 0.0,
409            emotional_intensity: 0.0,
410            session_depth: 0,
411            pattern_confidence: 0.0,
412            cache_hit_rate: 0.0,
413            archetype_match_score: 0.0,
414        }
415    }
416
417    #[test]
418    fn test_compute_attention_short_query() {
419        let state = AttentionState {
420            learned_weights: [1.0; NUM_LAYERS],
421            history: Vec::new(),
422            last_recall_ms: 0,
423            total_recalls: 0,
424        };
425        let mut signals = default_signals();
426        signals.query_length = 5; // short
427
428        let attn = state.compute_attention(&signals);
429        // Hebbian (0) and Archetype (3) should be elevated
430        assert!(attn.weights[0] > attn.weights[1]); // Hebbian > Mirror
431        assert!(attn.weights[3] > attn.weights[1]); // Archetype > Mirror
432    }
433
434    #[test]
435    fn test_compute_attention_long_query() {
436        let state = AttentionState {
437            learned_weights: [1.0; NUM_LAYERS],
438            history: Vec::new(),
439            last_recall_ms: 0,
440            total_recalls: 0,
441        };
442        let mut signals = default_signals();
443        signals.query_length = 50; // long
444
445        let attn = state.compute_attention(&signals);
446        // ThoughtGraph (5) should be elevated
447        assert!(attn.weights[5] > attn.weights[1]); // ThoughtGraph > Mirror
448    }
449
450    #[test]
451    fn test_compute_attention_high_emotion() {
452        let state = AttentionState {
453            learned_weights: [1.0; NUM_LAYERS],
454            history: Vec::new(),
455            last_recall_ms: 0,
456            total_recalls: 0,
457        };
458        let mut signals = default_signals();
459        signals.emotional_energy = 2.0;
460
461        let attn = state.compute_attention(&signals);
462        // Emotional (4) should be highest
463        assert!(attn.weights[4] > attn.weights[0]);
464    }
465
466    #[test]
467    fn test_compute_attention_deep_session() {
468        let state = AttentionState {
469            learned_weights: [1.0; NUM_LAYERS],
470            history: Vec::new(),
471            last_recall_ms: 0,
472            total_recalls: 0,
473        };
474        let mut signals = default_signals();
475        signals.session_depth = 10;
476
477        let attn = state.compute_attention(&signals);
478        // ThoughtGraph and PredictiveCache should be elevated
479        assert!(attn.weights[5] > attn.weights[1]);
480        assert!(attn.weights[6] > attn.weights[1]);
481    }
482
483    #[test]
484    fn test_normalization() {
485        let state = AttentionState {
486            learned_weights: [1.0; NUM_LAYERS],
487            history: Vec::new(),
488            last_recall_ms: 0,
489            total_recalls: 0,
490        };
491        let signals = default_signals();
492        let attn = state.compute_attention(&signals);
493
494        let sum: f32 = attn.weights.iter().sum();
495        assert!((sum - NUM_LAYERS as f32).abs() < 0.01);
496    }
497
498    #[test]
499    fn test_quality_inference_fast_requery() {
500        let state = AttentionState {
501            learned_weights: [1.0; NUM_LAYERS],
502            history: Vec::new(),
503            last_recall_ms: now_epoch_ms() - 2_000, // 2s ago
504            total_recalls: 1,
505        };
506        let q = state.infer_quality();
507        assert!(q < 0.3); // unsatisfied
508    }
509
510    #[test]
511    fn test_quality_inference_satisfied() {
512        let state = AttentionState {
513            learned_weights: [1.0; NUM_LAYERS],
514            history: Vec::new(),
515            last_recall_ms: now_epoch_ms() - 120_000, // 2 min ago
516            total_recalls: 1,
517        };
518        let q = state.infer_quality();
519        assert!((q - 1.0).abs() < 0.01);
520    }
521
522    #[test]
523    fn test_learned_weights_update() {
524        let mut state = AttentionState {
525            learned_weights: [1.0; NUM_LAYERS],
526            history: Vec::new(),
527            last_recall_ms: 0,
528            total_recalls: 0,
529        };
530
531        // Record some good outcomes with high Hebbian weight
532        let mut good_weights = [1.0f32; NUM_LAYERS];
533        good_weights[0] = 2.0; // Hebbian high
534        for _ in 0..5 {
535            state.record_outcome(0.9, &good_weights);
536        }
537
538        // Record some bad outcomes with high Mirror weight
539        let mut bad_weights = [1.0f32; NUM_LAYERS];
540        bad_weights[1] = 2.0; // Mirror high
541        for _ in 0..5 {
542            state.record_outcome(0.1, &bad_weights);
543        }
544
545        // Learned weights should now favor Hebbian over Mirror
546        assert!(state.learned_weights[0] > state.learned_weights[1]);
547    }
548
549    #[test]
550    fn test_save_load_roundtrip() {
551        let dir = tempfile::tempdir().unwrap();
552        let mut state = AttentionState {
553            learned_weights: [1.1, 0.9, 1.0, 1.3, 0.8, 1.2, 0.7],
554            history: Vec::new(),
555            last_recall_ms: 12345678,
556            total_recalls: 42,
557        };
558        state.history.push(AttentionOutcome {
559            weights: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
560            timestamp_ms: 999,
561            quality: 0.75,
562        });
563
564        state.save(dir.path()).unwrap();
565        let loaded = AttentionState::load_or_init(dir.path());
566
567        assert_eq!(loaded.total_recalls, 42);
568        assert_eq!(loaded.last_recall_ms, 12345678);
569        assert!((loaded.learned_weights[0] - 1.1).abs() < 0.001);
570        assert!((loaded.learned_weights[6] - 0.7).abs() < 0.001);
571        assert_eq!(loaded.history.len(), 1);
572        assert!((loaded.history[0].quality - 0.75).abs() < 0.001);
573    }
574
575    #[test]
576    fn test_history_cap() {
577        let mut state = AttentionState {
578            learned_weights: [1.0; NUM_LAYERS],
579            history: Vec::new(),
580            last_recall_ms: 0,
581            total_recalls: 0,
582        };
583        let weights = [1.0; NUM_LAYERS];
584        for _ in 0..MAX_HISTORY + 50 {
585            state.record_outcome(0.5, &weights);
586        }
587        assert!(state.history.len() <= MAX_HISTORY);
588    }
589}