1use std::collections::HashMap;
3use rand::{Rng, SeedableRng};
4use rand_chacha::ChaCha8Rng;
5
6pub type Hypervector = Vec<i8>;
7
8pub fn make_hv(seed: u64, dims: usize) -> Hypervector {
10 let mut rng = ChaCha8Rng::seed_from_u64(seed);
11 (0..dims).map(|_| if rng.gen_bool(0.5) { 1 } else { -1 }).collect()
12}
13
14pub fn bundle(hv: &mut Hypervector, other: &Hypervector) {
16 for (a, b) in hv.iter_mut().zip(other) {
17 *a += b;
18 }
19}
20
21pub fn bipolar_clamp(hv: &mut Hypervector) {
23 for v in hv.iter_mut() {
24 *v = v.signum();
25 }
26}
27
28pub fn dot(a: &Hypervector, b: &Hypervector) -> f64 {
30 let dot: i64 = a.iter().zip(b).map(|(x, y)| (*x as i64) * (*y as i64)).sum();
31 let norm_a = (a.iter().map(|x| (*x as i64).pow(2)).sum::<i64>() as f64).sqrt();
32 let norm_b = (b.iter().map(|x| (*x as i64).pow(2)).sum::<i64>() as f64).sqrt();
33 if norm_a == 0.0 || norm_b == 0.0 { 0.0 } else { dot as f64 / (norm_a * norm_b) }
34}
35
36#[derive(Debug, Clone)]
38pub struct MemoryRecord {
39 pub id: i64,
40 pub content: String,
41 pub source: String,
42 pub timestamp: i64,
43 pub tier: i32, pub recall_count: i32,
45 pub error_flag: i32,
46 pub entropy: f64,
47}
48
49#[derive(Debug, Clone)]
51pub struct ScoredMemory {
52 pub memory: MemoryRecord,
53 pub score: f64,
54}
55
56pub fn open_persistent(path: &str) -> Result<MemoryStore, String> {
58 let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
59 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
60 .map_err(|e| format!("PRAGMA: {}", e))?;
61 conn.execute_batch(
62 "CREATE TABLE IF NOT EXISTS cortex_memories (
63 id INTEGER PRIMARY KEY AUTOINCREMENT,
64 content TEXT NOT NULL,
65 source TEXT DEFAULT '',
66 timestamp INTEGER NOT NULL,
67 tier INTEGER DEFAULT 0,
68 recall_count INTEGER DEFAULT 0
69 );"
70 ).map_err(|e| format!("schema: {}", e))?;
71
72 let mut store = MemoryStore::new(100);
73
74 let mut stmt = conn.prepare("SELECT id, content, source, timestamp, tier, recall_count, 0, 0.0 FROM cortex_memories ORDER BY id").map_err(|e| format!("query: {}", e))?;
75 let rows = stmt.query_map([], |row| {
76 Ok(MemoryRecord {
77 id: row.get(0)?,
78 content: row.get(1)?,
79 source: row.get(2)?,
80 timestamp: row.get(3)?,
81 tier: row.get(4)?,
82 recall_count: row.get(5)?,
83 error_flag: row.get(6)?,
84 entropy: row.get(7)?,
85 })
86 }).map_err(|e| format!("rows: {}", e))?;
87
88 for row in rows.flatten() {
89 store.memories.push(row);
90 }
91 Ok(store)
92}
93
94pub fn save_persistent(store: &MemoryStore, path: &str) -> Result<(), String> {
96 let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
97 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
98 .map_err(|e| format!("PRAGMA: {}", e))?;
99 conn.execute_batch(
100 "CREATE TABLE IF NOT EXISTS cortex_memories (
101 id INTEGER PRIMARY KEY AUTOINCREMENT,
102 content TEXT NOT NULL,
103 source TEXT DEFAULT '',
104 timestamp INTEGER NOT NULL,
105 tier INTEGER DEFAULT 0,
106 recall_count INTEGER DEFAULT 0
107 );"
108 ).map_err(|e| format!("schema: {}", e))?;
109
110 let _ = conn.execute("DELETE FROM cortex_memories", []);
111 for m in &store.memories {
112 if let Err(e) = conn.execute(
113 "INSERT INTO cortex_memories (content, source, timestamp, tier, recall_count) VALUES (?1, ?2, ?3, ?4, ?5)",
114 rusqlite::params![m.content, m.source, m.timestamp, m.tier, m.recall_count],
115 ) {
116 eprintln!("[memory] save: {}", e);
117 }
118 }
119 Ok(())
120}
121
122pub struct MemoryStore {
123 pub memories: Vec<MemoryRecord>,
124 token_hvs: HashMap<String, Hypervector>,
125 cooccur: HashMap<(String, String), u64>,
126 pub dims: usize,
127}
128
129impl MemoryStore {
130 pub fn new(dims: usize) -> Self {
131 Self { memories: Vec::new(), token_hvs: HashMap::new(), cooccur: HashMap::new(), dims }
132 } pub fn ensure_token_hv(&mut self, token: &str) -> Hypervector {
133 let dims = self.dims;
134 self.token_hvs.entry(token.to_string())
135 .or_insert_with(|| make_hv(token.len() as u64, dims))
136 .clone()
137 }
138
139 pub fn retain(&mut self, content: &str, source: &str, error: bool) -> i64 {
141 let tokens = scan_tokens(content);
142 if tokens.is_empty() { return 0; }
143
144 for i in 0..tokens.len() {
146 for j in (i + 1)..tokens.len().min(i + 5) {
147 let key = (tokens[i].clone(), tokens[j].clone());
148 *self.cooccur.entry(key).or_insert(0) += 1;
149 self.hebbian_update(&tokens[i], &tokens[j]);
151 }
152 }
153
154 let now = chrono::Utc::now().timestamp();
155 let id = self.memories.len() as i64 + 1;
156 self.memories.push(MemoryRecord {
157 id,
158 content: content.to_string(),
159 source: source.to_string(),
160 timestamp: now,
161 tier: 0,
162 recall_count: 0,
163 error_flag: if error { 1 } else { 0 },
164 entropy: 0.0,
165 });
166 id
167 }
168
169 fn hebbian_update(&mut self, a: &str, b: &str) {
170 let mut hv_a = self.ensure_token_hv(a);
171 let mut hv_b = self.ensure_token_hv(b);
172 let epsilon = 0.1;
173 for i in 0..self.dims {
174 hv_a[i] = (hv_a[i] as f64 + epsilon * hv_b[i] as f64) as i8;
175 hv_b[i] = (hv_b[i] as f64 + epsilon * hv_a[i] as f64) as i8;
176 }
177 bipolar_clamp(&mut hv_a);
178 bipolar_clamp(&mut hv_b);
179 if let Some(stored) = self.token_hvs.get_mut(a) {
181 *stored = hv_a;
182 }
183 if let Some(stored) = self.token_hvs.get_mut(b) {
184 *stored = hv_b;
185 }
186 }
187
188 pub fn encode_query(&self, query: &str) -> Hypervector {
190 let tokens = scan_tokens(query);
191 let mut hv = vec![0i8; self.dims];
192 for t in tokens {
193 if let Some(thv) = self.token_hvs.get(&t) {
194 bundle(&mut hv, thv);
195 }
196 }
197 hv
198 }
199
200 pub fn recall(&self, query: &str, top_n: usize) -> Vec<ScoredMemory> {
202 if self.memories.is_empty() { return vec![]; }
203 let q_hv = self.encode_query(query);
204 let mut scored: Vec<ScoredMemory> = self.memories.iter().map(|m| {
205 let m_hv = self.encode_query(&m.content);
206 let score = dot(&q_hv, &m_hv);
207 ScoredMemory { memory: m.clone(), score }
208 }).collect();
209 scored.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
210 scored.truncate(top_n);
211 scored
212 }
213
214 pub fn predict(&self, query: &str, top_n: usize) -> Vec<(String, u64)> {
216 let tokens = scan_tokens(query);
217 let mut scores: HashMap<String, u64> = HashMap::new();
218 for t in &tokens {
219 for ((a, b), count) in &self.cooccur {
220 if a == t { *scores.entry(b.clone()).or_insert(0) += count; }
221 if b == t { *scores.entry(a.clone()).or_insert(0) += count; }
222 }
223 }
224 let mut sorted: Vec<(String, u64)> = scores.into_iter().collect();
225 sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
226 sorted.truncate(top_n);
227 sorted
228 }
229
230 pub fn consolidate(&mut self) {
232 let now = chrono::Utc::now().timestamp();
233 for m in &mut self.memories {
234 if m.recall_count >= 10 && m.tier < 2 { m.tier = 2; }
235 if m.recall_count >= 3 && m.tier < 1 && now - m.timestamp < 7 * 86400 { m.tier = 1; }
236 }
237 }
238}
239
240fn scan_tokens(text: &str) -> Vec<String> {
242 text.split(|c: char| !c.is_alphanumeric() && c != '_')
243 .filter(|t| t.len() >= 3 && t.chars().any(|c| c.is_alphabetic()))
244 .map(|t| t.to_lowercase())
245 .collect()
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_make_hv() {
254 let hv = make_hv(42, 10000);
255 assert_eq!(hv.len(), 10000);
256 assert!(hv.contains(&1));
257 assert!(hv.contains(&-1));
258 }
259
260 #[test]
261 fn test_bundle_and_clamp() {
262 let mut a = vec![1i8; 100];
263 let b = vec![-1i8; 100];
264 bundle(&mut a, &b);
265 for v in &a { assert_eq!(*v, 0i8); }
266 }
267
268 #[test]
269 fn test_retain_and_recall() {
270 let mut store = MemoryStore::new(100);
271 store.retain("Alice works at Google", "user", false);
272 let results = store.recall("Alice", 5);
273 assert_eq!(results.len(), 1);
274 assert!(results[0].score > 0.0);
275 }
276
277 #[test]
278 fn test_prediction() {
279 let mut store = MemoryStore::new(100);
280 store.retain("Alice works at Google", "user", false);
281 store.retain("Bob works at Google too", "user", false);
282 let preds = store.predict("Alice", 5);
283 assert!(!preds.is_empty());
284 assert!(preds.iter().any(|(t, _)| t.contains("google")));
285 }
286
287 #[test]
288 fn test_empty_store() {
289 let store = MemoryStore::new(100);
290 assert!(store.recall("anything", 5).is_empty());
291 }
292}