Skip to main content

microscope_memory/
curiosity.rs

1//! Proactive Curiosity Module for Microscope Memory.
2//!
3//! The system autonomously identifies interesting blocks and generates queries.
4//! Uses: Eureka surprise, Hebbian energy, spaced repetition due/overdue,
5//! emotional charge, and low familiarity to find "curious" signals.
6//!
7//! Binary format: CUR1
8//!   magic: "CUR1" (4 bytes)
9//!   timestamp_ms: u64 (8 bytes)
10//!   query_len: u16 (2 bytes) + query bytes
11//!   reason_len: u16 (2 bytes) + reason bytes
12//!   score: f32 (4 bytes)
13
14use std::fs;
15use std::path::Path;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use crate::config::Config;
19use crate::hebbian::HebbianState;
20use crate::reader::MicroscopeReader;
21use crate::spaced_repetition::SpacedRepetition;
22use colored::Colorize;
23
24const MAX_CURIOSITY_QUERIES: usize = 20;
25const MAX_STR_LEN: usize = 256;
26
27#[derive(Clone, Debug)]
28pub struct CuriosityQuery {
29    pub timestamp_ms: u64,
30    pub query: String,
31    pub reason: String,
32    pub score: f32,
33}
34
35impl CuriosityQuery {
36    pub fn to_bytes(&self) -> Vec<u8> {
37        let qb = self.query.as_bytes();
38        let rb = self.reason.as_bytes();
39        let qlen = qb.len().min(MAX_STR_LEN) as u16;
40        let rlen = rb.len().min(MAX_STR_LEN) as u16;
41        let mut buf = Vec::with_capacity(20 + qlen as usize + rlen as usize);
42        buf.extend_from_slice(b"CUR1");
43        buf.extend_from_slice(&self.timestamp_ms.to_le_bytes());
44        buf.extend_from_slice(&qlen.to_le_bytes());
45        buf.extend_from_slice(&qb[..qlen as usize]);
46        buf.extend_from_slice(&rlen.to_le_bytes());
47        buf.extend_from_slice(&rb[..rlen as usize]);
48        buf.extend_from_slice(&self.score.to_le_bytes());
49        buf
50    }
51
52    pub fn from_bytes(data: &[u8]) -> Option<Self> {
53        if data.len() < 20 || &data[0..4] != b"CUR1" {
54            return None;
55        }
56        let mut pos = 4;
57        let ts = u64::from_le_bytes(data[pos..pos + 8].try_into().ok()?);
58        pos += 8;
59        let qlen = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap_or([0; 2])) as usize;
60        pos += 2;
61        let query = if qlen > 0 {
62            String::from_utf8_lossy(&data[pos..pos + qlen]).to_string()
63        } else {
64            String::new()
65        };
66        pos += qlen;
67        let rlen = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap_or([0; 2])) as usize;
68        pos += 2;
69        let reason = if rlen > 0 {
70            String::from_utf8_lossy(&data[pos..pos + rlen]).to_string()
71        } else {
72            String::new()
73        };
74        pos += rlen;
75        let score = f32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
76        Some(Self {
77            timestamp_ms: ts,
78            query,
79            reason,
80            score,
81        })
82    }
83}
84
85pub struct CuriosityState {
86    pub queries: Vec<CuriosityQuery>,
87}
88
89impl CuriosityState {
90    pub fn load_or_init(output_dir: &Path) -> Self {
91        let path = output_dir.join("curiosity.bin");
92        let mut queries = Vec::new();
93        if let Ok(data) = fs::read(&path) {
94            let mut pos = 0;
95            while pos + 20 <= data.len() {
96                if let Some(q) = CuriosityQuery::from_bytes(&data[pos..]) {
97                    let size = 4
98                        + 8
99                        + 2
100                        + q.query.len().min(MAX_STR_LEN)
101                        + 2
102                        + q.reason.len().min(MAX_STR_LEN)
103                        + 4;
104                    pos += size;
105                    queries.push(q);
106                } else {
107                    pos += 1;
108                }
109            }
110        }
111        Self { queries }
112    }
113
114    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
115        let path = output_dir.join("curiosity.bin");
116        let mut buf = Vec::new();
117        for q in self.queries.iter().rev().take(MAX_CURIOSITY_QUERIES).rev() {
118            buf.extend_from_slice(&q.to_bytes());
119        }
120        let tmp_path = output_dir.join("curiosity.bin.tmp");
121        fs::write(&tmp_path, &buf).map_err(|e| format!("write curiosity.bin: {}", e))?;
122        fs::rename(&tmp_path, &path).map_err(|e| format!("rename curiosity.bin: {}", e))
123    }
124
125    pub fn generate_queries(
126        &mut self,
127        _config: &Config,
128        reader: &MicroscopeReader,
129        output_dir: &Path,
130    ) -> Vec<CuriosityQuery> {
131        let now_ms = SystemTime::now()
132            .duration_since(UNIX_EPOCH)
133            .unwrap_or_default()
134            .as_millis() as u64;
135        let hebb = HebbianState::load_or_init(output_dir, reader.block_count);
136        let spaced = SpacedRepetition::load_or_init(output_dir);
137        let mut new_queries = Vec::new();
138
139        // 1. Check due blocks - memories that need review
140        let due = spaced.due_blocks();
141        if !due.is_empty() {
142            let texts: Vec<String> = due
143                .iter()
144                .take(3)
145                .map(|&idx| {
146                    let text = reader.text(idx as usize);
147                    crate::safe_truncate(text, 60)
148                })
149                .collect();
150            if !texts.is_empty() {
151                new_queries.push(CuriosityQuery {
152                    timestamp_ms: now_ms,
153                    query: format!("Why have I not recalled {} recently?", texts.join(", ")),
154                    reason: format!(
155                        "{} memories are due for spaced repetition review",
156                        due.len()
157                    ),
158                    score: 0.5 + (due.len() as f32 * 0.05).min(0.5),
159                });
160            }
161        }
162
163        // 2. Find hot blocks with high energy but low familiarity
164        let mut hot_blocks: Vec<(u32, f32)> = hebb
165            .activations
166            .iter()
167            .enumerate()
168            .filter(|(_, a)| a.energy > 0.2)
169            .map(|(i, a)| (i as u32, a.energy))
170            .collect();
171        hot_blocks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
172        for &(idx, energy) in hot_blocks.iter().take(2) {
173            let text = reader.text(idx as usize);
174            let preview = crate::safe_truncate(text, 50);
175            new_queries.push(CuriosityQuery {
176                timestamp_ms: now_ms,
177                query: format!("What makes '{}' so active?", preview),
178                reason: format!(
179                    "block {} has energy {:.2} - highest in the system",
180                    idx, energy
181                ),
182                score: energy * 0.8,
183            });
184        }
185
186        // 3. Check archetypes - emerging patterns
187        let archetypes = crate::archetype::ArchetypeState::load_or_init(output_dir);
188        if !archetypes.archetypes.is_empty() {
189            for arch in archetypes.archetypes.iter().take(2) {
190                new_queries.push(CuriosityQuery {
191                    timestamp_ms: now_ms,
192                    query: format!(
193                        "What is the '{}' archetype and why is it forming?",
194                        arch.label
195                    ),
196                    reason: format!(
197                        "archetype '{}' has {} members and strength {:.3}",
198                        arch.label,
199                        arch.members.len(),
200                        arch.strength
201                    ),
202                    score: arch.strength * 0.7,
203                });
204            }
205        }
206
207        // 4. Check thought patterns
208        let thought_graph = crate::thought_graph::ThoughtGraphState::load_or_init(output_dir);
209        let patterns = thought_graph.top_patterns(2);
210        for p in patterns {
211            new_queries.push(CuriosityQuery {
212                timestamp_ms: now_ms,
213                query: "What connects the blocks in this thought pattern?".to_string(),
214                reason: format!(
215                    "thought pattern with {} blocks, frequency {}",
216                    p.result_blocks.len(),
217                    p.frequency
218                ),
219                score: (p.frequency as f32) * 0.1,
220            });
221        }
222
223        // 5. General curiosity about system state
224        new_queries.push(CuriosityQuery {
225            timestamp_ms: now_ms,
226            query: "How am I changing over time?".to_string(),
227            reason: format!(
228                "{} total blocks, {} hot memories, {} due for review",
229                reader.block_count,
230                hot_blocks.len(),
231                due.len()
232            ),
233            score: 0.3,
234        });
235
236        self.queries.extend(new_queries.clone());
237        if self.queries.len() > MAX_CURIOSITY_QUERIES * 2 {
238            self.queries = self.queries[self.queries.len() - MAX_CURIOSITY_QUERIES..].to_vec();
239        }
240        let _ = self.save(output_dir);
241        new_queries
242    }
243}
244
245pub fn format_curiosity(queries: &[CuriosityQuery]) -> String {
246    let mut out = format!("  {} I am curious about:\n", "CURIOUS:".yellow().bold());
247    for q in queries.iter().take(5) {
248        out.push_str(&format!(
249            "    \x20 [{:.2}] {} ({})\n",
250            q.score, q.query, q.reason
251        ));
252    }
253    out
254}