1use std::fs;
11use std::path::Path;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14const DEFAULT_CAPACITY: usize = 7; const DECAY_MS: u64 = 30_000; const CONSOLIDATE_ACCESS_THRESHOLD: u32 = 3; const PRIMACY_BOOST: f32 = 0.15; const RECENCY_BOOST: f32 = 0.10; #[repr(u8)]
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum MemoryType {
24 Episodic = 0,
25 Semantic = 1,
26 Implicit = 2,
27 Explicit = 3,
28}
29
30#[derive(Clone, Debug)]
32pub struct WorkingMemoryItem {
33 pub text: String,
34 pub timestamp_ms: u64,
35 pub last_access_ms: u64,
36 pub importance: f32,
37 pub decay_rate: f32,
38 pub serial_pos: u8,
39 pub memory_type: MemoryType,
40 pub access_count: u32,
41 pub layer: String,
42}
43
44pub struct WorkingMemory {
46 pub items: Vec<WorkingMemoryItem>,
47 pub capacity: usize,
48 pub decay_ms: u64,
49}
50
51pub struct WorkingMemoryStats {
53 pub item_count: usize,
54 pub capacity: usize,
55 pub hot_items: usize, pub decay_ms: u64,
57 pub consolidation_candidates: usize, }
59impl WorkingMemory {
60 pub fn load_or_init(output_dir: &Path) -> Self {
62 let path = output_dir.join("working_memory.bin");
63 if let Ok(data) = fs::read(&path) {
64 if data.len() >= 6 && &data[0..4] == b"WKM1" {
65 let capacity = data[4] as usize;
66 let decay_ms = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as u64;
67 let count = data[9] as usize;
68 let mut items = Vec::with_capacity(count);
69 let mut off = 10;
70 for _ in 0..count {
71 if off + 2 > data.len() {
72 break;
73 }
74 let text_len = u16::from_le_bytes([data[off], data[off + 1]]) as usize;
75 off += 2;
76 if off + text_len > data.len() {
77 break;
78 }
79 let text = String::from_utf8_lossy(&data[off..off + text_len]).to_string();
80 off += text_len;
81 if off + 37 > data.len() {
82 break;
83 }
84 let ts = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
85 let la = u64::from_le_bytes(data[off + 8..off + 16].try_into().unwrap());
86 let imp = f32::from_le_bytes(data[off + 16..off + 20].try_into().unwrap());
87 let dr = f32::from_le_bytes(data[off + 20..off + 24].try_into().unwrap());
88 let sp = data[off + 24];
89 let mt = match data[off + 25] {
90 0 => MemoryType::Episodic,
91 1 => MemoryType::Semantic,
92 2 => MemoryType::Implicit,
93 3 => MemoryType::Explicit,
94 _ => MemoryType::Episodic,
95 };
96 let ac = u32::from_le_bytes(data[off + 26..off + 30].try_into().unwrap());
97 let ll = data[off + 30] as usize;
98 off += 31;
99 let layer = if off + ll <= data.len() {
100 String::from_utf8_lossy(&data[off..off + ll]).to_string()
101 } else {
102 String::new()
103 };
104 off += ll;
105 items.push(WorkingMemoryItem {
106 text,
107 timestamp_ms: ts,
108 last_access_ms: la,
109 importance: imp,
110 decay_rate: dr,
111 serial_pos: sp,
112 memory_type: mt,
113 access_count: ac,
114 layer,
115 });
116 }
117 return WorkingMemory {
118 items,
119 capacity,
120 decay_ms,
121 };
122 }
123 }
124 WorkingMemory {
125 items: Vec::new(),
126 capacity: DEFAULT_CAPACITY,
127 decay_ms: DECAY_MS,
128 }
129 }
130
131 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
133 let path = output_dir.join("working_memory.bin");
134 let mut buf = Vec::with_capacity(64 + self.items.len() * 128);
135 buf.extend_from_slice(b"WKM1");
136 buf.push(self.capacity as u8);
137 buf.extend_from_slice(&(self.decay_ms as u32).to_le_bytes());
138 buf.push(self.items.len() as u8);
139 for item in &self.items {
140 let text_bytes = item.text.as_bytes();
141 let text_len = text_bytes.len().min(u16::MAX as usize);
142 buf.extend_from_slice(&(text_len as u16).to_le_bytes());
143 buf.extend_from_slice(&text_bytes[..text_len]);
144 buf.extend_from_slice(&item.timestamp_ms.to_le_bytes());
145 buf.extend_from_slice(&item.last_access_ms.to_le_bytes());
146 buf.extend_from_slice(&item.importance.to_le_bytes());
147 buf.extend_from_slice(&item.decay_rate.to_le_bytes());
148 buf.push(item.serial_pos);
149 buf.push(item.memory_type as u8);
150 buf.extend_from_slice(&item.access_count.to_le_bytes());
151 let layer_bytes = item.layer.as_bytes();
152 let ll = layer_bytes.len().min(255);
153 buf.push(ll as u8);
154 buf.extend_from_slice(&layer_bytes[..ll]);
155 }
156 let tmp_path = output_dir.join("working_memory.bin.tmp");
157 fs::write(&tmp_path, &buf).map_err(|e| format!("write working_memory.bin: {}", e))?;
158 fs::rename(&tmp_path, &path).map_err(|e| format!("rename working_memory.bin: {}", e))
159 }
160 pub fn push(&mut self, text: &str, importance: f32, layer: &str, mem_type: MemoryType) {
162 let now = now_ms();
163 if self.items.len() >= self.capacity {
165 let mut worst = 0;
166 let mut worst_score = f32::MAX;
167 for (i, item) in self.items.iter().enumerate() {
168 let recency =
169 ((now - item.last_access_ms) as f64 / self.decay_ms as f64).max(0.0) as f32;
170 let age_weight = if recency > 1.0 {
171 0.1
172 } else {
173 1.0 - recency * 0.9
174 };
175 let score = item.importance * age_weight;
176 if score < worst_score {
177 worst_score = score;
178 worst = i;
179 }
180 }
181 self.items.remove(worst);
182 }
183 self.items.push(WorkingMemoryItem {
184 text: text.to_string(),
185 timestamp_ms: now,
186 last_access_ms: now,
187 importance,
188 decay_rate: self.decay_ms as f32 / DEFAULT_CAPACITY as f32,
189 serial_pos: self.items.len() as u8,
190 memory_type: mem_type,
191 access_count: 0,
192 layer: layer.to_string(),
193 });
194 }
195
196 pub fn decay(&mut self) {
198 let now = now_ms();
199 self.items.retain(|item| {
200 let elapsed = (now - item.last_access_ms) as f32;
201 let age_ratio = elapsed / self.decay_ms as f32;
202 age_ratio < 1.0 || item.access_count >= CONSOLIDATE_ACCESS_THRESHOLD
203 });
204 }
205
206 pub fn access(&mut self, idx: usize) -> Option<&WorkingMemoryItem> {
209 if idx >= self.items.len() {
210 return None;
211 }
212 let recency_boost = idx == self.items.len() - 1;
213 let item = &mut self.items[idx];
214 item.last_access_ms = now_ms();
215 item.access_count += 1;
216 if item.serial_pos == 0 {
217 item.importance += PRIMACY_BOOST;
218 }
219 if recency_boost {
220 item.importance += RECENCY_BOOST;
221 }
222 None
224 }
225
226 pub fn find(&self, query: &str) -> Option<usize> {
228 let lower = query.to_lowercase();
229 self.items
230 .iter()
231 .position(|item| item.text.to_lowercase().contains(&lower))
232 }
233
234 pub fn consolidate(&mut self) -> Vec<WorkingMemoryItem> {
237 let mut consolidated = Vec::new();
238 let mut i = 0;
239 while i < self.items.len() {
240 if self.items[i].access_count >= CONSOLIDATE_ACCESS_THRESHOLD {
241 consolidated.push(self.items.remove(i));
242 } else {
243 i += 1;
244 }
245 }
246 consolidated
247 }
248
249 pub fn stats(&self) -> WorkingMemoryStats {
251 let hot = self.items.iter().filter(|i| i.access_count >= 2).count();
252 let cons_candidates = self
253 .items
254 .iter()
255 .filter(|i| i.access_count >= CONSOLIDATE_ACCESS_THRESHOLD)
256 .count();
257 WorkingMemoryStats {
258 item_count: self.items.len(),
259 capacity: self.capacity,
260 hot_items: hot,
261 decay_ms: self.decay_ms,
262 consolidation_candidates: cons_candidates,
263 }
264 }
265
266 pub fn clear(&mut self) {
268 self.items.clear();
269 }
270}
271
272fn now_ms() -> u64 {
274 SystemTime::now()
275 .duration_since(UNIX_EPOCH)
276 .unwrap_or_default()
277 .as_millis() as u64
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use std::path::Path;
284
285 #[test]
286 fn test_default_state() {
287 let wm = WorkingMemory::load_or_init(Path::new("."));
288 assert!(wm.items.is_empty());
289 assert_eq!(wm.capacity, DEFAULT_CAPACITY);
290 assert_eq!(wm.decay_ms, DECAY_MS);
291 }
292
293 #[test]
294 fn test_push_and_count() {
295 let mut wm = WorkingMemory::load_or_init(Path::new("."));
296 wm.push("first item", 5.0, "short_term", MemoryType::Episodic);
297 wm.push("second item", 3.0, "long_term", MemoryType::Semantic);
298 assert_eq!(wm.items.len(), 2);
299 }
300
301 #[test]
302 fn test_eviction_when_full() {
303 let mut wm = WorkingMemory {
304 items: vec![],
305 capacity: 3,
306 decay_ms: 30000,
307 };
308 wm.push("a", 1.0, "st", MemoryType::Episodic);
309 wm.push("b", 1.0, "st", MemoryType::Episodic);
310 wm.push("c", 1.0, "st", MemoryType::Episodic);
311 wm.push("d", 1.0, "st", MemoryType::Episodic);
312 assert_eq!(wm.items.len(), 3);
313 }
314
315 #[test]
316 fn test_access_bumps_count() {
317 let mut wm = WorkingMemory {
318 items: vec![],
319 capacity: 7,
320 decay_ms: 30000,
321 };
322 wm.push("test", 5.0, "st", MemoryType::Episodic);
323 wm.access(0);
324 assert_eq!(wm.items[0].access_count, 1);
325 }
326
327 #[test]
328 fn test_decay_removes_old() {
329 let mut wm = WorkingMemory {
330 items: vec![],
331 capacity: 7,
332 decay_ms: 1,
333 }; wm.push("old", 5.0, "st", MemoryType::Episodic);
335 std::thread::sleep(std::time::Duration::from_millis(5));
336 wm.decay();
337 assert!(wm.items.is_empty());
338 }
339
340 #[test]
341 fn test_stats() {
342 let mut wm = WorkingMemory {
343 items: vec![],
344 capacity: 5,
345 decay_ms: 30000,
346 };
347 wm.push("a", 5.0, "st", MemoryType::Episodic);
348 wm.push("b", 3.0, "lt", MemoryType::Semantic);
349 let s = wm.stats();
350 assert_eq!(s.item_count, 2);
351 assert_eq!(s.capacity, 5);
352 }
353
354 #[test]
355 fn test_save_load_roundtrip() {
356 let dir = std::env::temp_dir().join("microscope_wm_test");
357 let _ = std::fs::create_dir_all(&dir);
358 let mut wm = WorkingMemory::load_or_init(&dir);
359 wm.push("hello", 7.0, "short_term", MemoryType::Episodic);
360 wm.save(&dir).unwrap();
361
362 let loaded = WorkingMemory::load_or_init(&dir);
363 assert_eq!(loaded.items.len(), 1);
364 assert_eq!(loaded.items[0].text, "hello");
365 assert!((loaded.items[0].importance - 7.0).abs() < 0.01);
366
367 let _ = std::fs::remove_dir_all(&dir);
368 }
369
370 #[test]
371 fn test_clear() {
372 let mut wm = WorkingMemory {
373 items: vec![],
374 capacity: 7,
375 decay_ms: 30000,
376 };
377 wm.push("x", 5.0, "st", MemoryType::Episodic);
378 wm.clear();
379 assert!(wm.items.is_empty());
380 }
381}