Skip to main content

reliary_memory/
lib.rs

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