1use chrono::{DateTime, Duration, Utc};
2use rusqlite::{params, Connection, OptionalExtension, Result as SqlResult};
3use sha2::{Digest, Sha256};
4use std::path::Path;
5
6use crate::memory::{AuditEntry, CorruptedMemory, Episode, Fact, MemoryKind, MemoryRecord, MemoryStats, VerifyResult};
7
8pub struct MemoryStore {
9 conn: Connection,
10}
11
12impl MemoryStore {
13 pub fn conn(&self) -> &Connection {
14 &self.conn
15 }
16
17 pub fn open<P: AsRef<Path>>(path: P) -> SqlResult<Self> {
18 let conn = Connection::open(path)?;
19 let store = Self { conn };
20 store.init_schema()?;
21 Ok(store)
22 }
23
24 pub fn open_in_memory() -> SqlResult<Self> {
25 let conn = Connection::open_in_memory()?;
26 let store = Self { conn };
27 store.init_schema()?;
28 Ok(store)
29 }
30
31 fn init_schema(&self) -> SqlResult<()> {
32 self.conn.execute_batch(
33 "CREATE TABLE IF NOT EXISTS memories (
34 id INTEGER PRIMARY KEY AUTOINCREMENT,
35 kind TEXT NOT NULL CHECK(kind IN ('fact', 'episode')),
36 subject TEXT,
37 relation TEXT,
38 object TEXT,
39 episode_text TEXT,
40 strength REAL NOT NULL DEFAULT 1.0,
41 embedding BLOB,
42 created_at TEXT NOT NULL,
43 last_accessed_at TEXT NOT NULL,
44 access_count INTEGER NOT NULL DEFAULT 0
45 );
46 CREATE INDEX IF NOT EXISTS idx_memories_subject ON memories(subject);
47 CREATE INDEX IF NOT EXISTS idx_memories_kind ON memories(kind);",
48 )?;
49 let has_tags: bool = self.conn
51 .prepare("SELECT tags FROM memories LIMIT 0")
52 .is_ok();
53 if !has_tags {
54 self.conn.execute_batch(
55 "ALTER TABLE memories ADD COLUMN tags TEXT NOT NULL DEFAULT '';"
56 )?;
57 }
58 let has_source: bool = self.conn
60 .prepare("SELECT source FROM memories LIMIT 0")
61 .is_ok();
62 if !has_source {
63 self.conn.execute_batch(
64 "ALTER TABLE memories ADD COLUMN source TEXT;
65 ALTER TABLE memories ADD COLUMN session_id TEXT;
66 ALTER TABLE memories ADD COLUMN channel TEXT;"
67 )?;
68 }
69 let has_importance: bool = self.conn
71 .prepare("SELECT importance FROM memories LIMIT 0")
72 .is_ok();
73 if !has_importance {
74 self.conn.execute_batch(
75 "ALTER TABLE memories ADD COLUMN importance REAL NOT NULL DEFAULT 0.5;"
76 )?;
77 }
78 let has_namespace: bool = self.conn
80 .prepare("SELECT namespace FROM memories LIMIT 0")
81 .is_ok();
82 if !has_namespace {
83 self.conn.execute_batch(
84 "ALTER TABLE memories ADD COLUMN namespace TEXT NOT NULL DEFAULT 'default';"
85 )?;
86 }
87 self.conn.execute_batch(
88 "CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories(namespace);"
89 )?;
90 let has_checksum: bool = self.conn
92 .prepare("SELECT checksum FROM memories LIMIT 0")
93 .is_ok();
94 if !has_checksum {
95 self.conn.execute_batch(
96 "ALTER TABLE memories ADD COLUMN checksum TEXT;"
97 )?;
98 }
99 self.conn.execute_batch(
101 "CREATE TABLE IF NOT EXISTS audit_log (
102 id INTEGER PRIMARY KEY AUTOINCREMENT,
103 timestamp TEXT NOT NULL,
104 action TEXT NOT NULL,
105 memory_id INTEGER,
106 actor TEXT NOT NULL DEFAULT 'system',
107 details_json TEXT
108 );
109 CREATE INDEX IF NOT EXISTS idx_audit_log_memory_id ON audit_log(memory_id);
110 CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor);
111 CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp);"
112 )?;
113 Ok(())
114 }
115
116 pub fn remember_fact(&self, subject: &str, relation: &str, object: &str, embedding: Option<&[f32]>) -> SqlResult<i64> {
119 self.remember_fact_with_tags(subject, relation, object, embedding, &[])
120 }
121
122 pub fn remember_fact_with_tags(&self, subject: &str, relation: &str, object: &str, embedding: Option<&[f32]>, tags: &[String]) -> SqlResult<i64> {
123 self.remember_fact_full(subject, relation, object, embedding, tags, None, None, None)
124 }
125
126 #[allow(clippy::too_many_arguments)]
127 pub fn remember_fact_full(
128 &self, subject: &str, relation: &str, object: &str, embedding: Option<&[f32]>,
129 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
130 ) -> SqlResult<i64> {
131 self.remember_fact_ns(subject, relation, object, embedding, tags, source, session_id, channel, "default")
132 }
133
134 #[allow(clippy::too_many_arguments)]
135 pub fn remember_fact_ns(
136 &self, subject: &str, relation: &str, object: &str, embedding: Option<&[f32]>,
137 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
138 namespace: &str,
139 ) -> SqlResult<i64> {
140 let now = Utc::now().to_rfc3339();
141 let emb_blob = embedding.map(embedding_to_blob);
142 let tags_str = tags.join(",");
143 let content = format!("{subject} {relation} {object}");
144 let checksum = compute_checksum(&content);
145 self.conn.execute(
146 "INSERT INTO memories (kind, subject, relation, object, embedding, created_at, last_accessed_at, tags, source, session_id, channel, namespace, checksum)
147 VALUES ('fact', ?1, ?2, ?3, ?4, ?5, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
148 params![subject, relation, object, emb_blob, now, tags_str, source, session_id, channel, namespace, checksum],
149 )?;
150 let id = self.conn.last_insert_rowid();
151 self.log_audit("remember", Some(id), "system", Some(&format!("{{\"kind\":\"fact\",\"subject\":{},\"relation\":{},\"object\":{},\"namespace\":{}}}", serde_json::json!(subject), serde_json::json!(relation), serde_json::json!(object), serde_json::json!(namespace))))?;
152 Ok(id)
153 }
154
155 #[allow(clippy::too_many_arguments)]
161 pub fn upsert_fact(
162 &self, subject: &str, relation: &str, object: &str, embedding: Option<&[f32]>,
163 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
164 ) -> SqlResult<(i64, bool)> {
165 self.upsert_fact_ns(subject, relation, object, embedding, tags, source, session_id, channel, "default")
166 }
167
168 #[allow(clippy::too_many_arguments)]
169 pub fn upsert_fact_ns(
170 &self, subject: &str, relation: &str, object: &str, embedding: Option<&[f32]>,
171 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
172 namespace: &str,
173 ) -> SqlResult<(i64, bool)> {
174 let existing_id: Option<i64> = self.conn.query_row(
176 "SELECT id FROM memories WHERE kind = 'fact' AND subject = ?1 AND relation = ?2 AND namespace = ?3 LIMIT 1",
177 params![subject, relation, namespace],
178 |row| row.get(0),
179 ).optional()?;
180
181 if let Some(id) = existing_id {
182 let now = Utc::now().to_rfc3339();
184 let emb_blob = embedding.map(embedding_to_blob);
185 let tags_str = tags.join(",");
186 let content = format!("{subject} {relation} {object}");
187 let checksum = compute_checksum(&content);
188 self.conn.execute(
189 "UPDATE memories SET object = ?1, embedding = COALESCE(?2, embedding), \
190 last_accessed_at = ?3, access_count = access_count + 1, \
191 tags = ?4, source = COALESCE(?5, source), \
192 session_id = COALESCE(?6, session_id), channel = COALESCE(?7, channel), \
193 checksum = ?8 \
194 WHERE id = ?9",
195 params![object, emb_blob, now, tags_str, source, session_id, channel, checksum, id],
196 )?;
197 self.log_audit("update", Some(id), "system", Some(&format!("{{\"kind\":\"fact\",\"subject\":{},\"relation\":{},\"object\":{},\"namespace\":{}}}", serde_json::json!(subject), serde_json::json!(relation), serde_json::json!(object), serde_json::json!(namespace))))?;
198 Ok((id, true))
199 } else {
200 let id = self.remember_fact_ns(subject, relation, object, embedding, tags, source, session_id, channel, namespace)?;
201 Ok((id, false))
202 }
203 }
204
205 pub fn remember_episode(&self, text: &str, embedding: Option<&[f32]>) -> SqlResult<i64> {
206 self.remember_episode_with_tags(text, embedding, &[])
207 }
208
209 pub fn remember_episode_with_tags(&self, text: &str, embedding: Option<&[f32]>, tags: &[String]) -> SqlResult<i64> {
210 self.remember_episode_full(text, embedding, tags, None, None, None)
211 }
212
213 #[allow(clippy::too_many_arguments)]
214 pub fn remember_episode_full(
215 &self, text: &str, embedding: Option<&[f32]>,
216 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
217 ) -> SqlResult<i64> {
218 self.remember_episode_ns(text, embedding, tags, source, session_id, channel, "default")
219 }
220
221 #[allow(clippy::too_many_arguments)]
222 pub fn remember_episode_ns(
223 &self, text: &str, embedding: Option<&[f32]>,
224 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
225 namespace: &str,
226 ) -> SqlResult<i64> {
227 let now = Utc::now().to_rfc3339();
228 let emb_blob = embedding.map(embedding_to_blob);
229 let tags_str = tags.join(",");
230 let checksum = compute_checksum(text);
231 self.conn.execute(
232 "INSERT INTO memories (kind, episode_text, embedding, created_at, last_accessed_at, tags, source, session_id, channel, namespace, checksum)
233 VALUES ('episode', ?1, ?2, ?3, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
234 params![text, emb_blob, now, tags_str, source, session_id, channel, namespace, checksum],
235 )?;
236 let id = self.conn.last_insert_rowid();
237 self.log_audit("remember", Some(id), "system", Some(&format!("{{\"kind\":\"episode\",\"namespace\":{}}}", serde_json::json!(namespace))))?;
238 Ok(id)
239 }
240
241 pub fn all_memories_with_text(&self) -> SqlResult<Vec<(MemoryRecord, String)>> {
244 self.all_memories_with_text_ns("default")
245 }
246
247 pub fn all_memories_with_text_ns(&self, namespace: &str) -> SqlResult<Vec<(MemoryRecord, String)>> {
248 let mut stmt = self.conn.prepare(
249 "SELECT id, kind, subject, relation, object, episode_text,
250 strength, embedding, created_at, last_accessed_at, access_count,
251 tags, source, session_id, channel, importance, namespace, checksum
252 FROM memories WHERE strength > 0.01 AND namespace = ?1",
253 )?;
254 let rows = stmt.query_map(params![namespace], |row| {
255 let mem = row_to_memory(row)?;
256 let text = mem.text_for_embedding();
257 Ok((mem, text))
258 })?;
259 rows.collect()
260 }
261
262 pub fn all_memories_with_text_filtered_by_tag(&self, tag: &str) -> SqlResult<Vec<(MemoryRecord, String)>> {
263 self.all_memories_with_text_filtered_by_tag_ns(tag, "default")
264 }
265
266 pub fn all_memories_with_text_filtered_by_tag_ns(&self, tag: &str, namespace: &str) -> SqlResult<Vec<(MemoryRecord, String)>> {
267 let pattern = format!("%{}%", tag);
268 let mut stmt = self.conn.prepare(
269 "SELECT id, kind, subject, relation, object, episode_text,
270 strength, embedding, created_at, last_accessed_at, access_count,
271 tags, source, session_id, channel, importance, namespace, checksum
272 FROM memories WHERE strength > 0.01 AND tags LIKE ?1 AND namespace = ?2",
273 )?;
274 let rows = stmt.query_map(params![pattern, namespace], |row| {
275 let mem = row_to_memory(row)?;
276 let text = mem.text_for_embedding();
277 Ok((mem, text))
278 })?;
279 let all: Vec<(MemoryRecord, String)> = rows.collect::<SqlResult<Vec<_>>>()?;
281 Ok(all.into_iter().filter(|(m, _)| m.tags.iter().any(|t| t == tag)).collect())
282 }
283
284 pub fn touch_memory_with_strength(&self, id: i64, strength: f64, now: DateTime<Utc>) -> SqlResult<()> {
285 self.conn.execute(
286 "UPDATE memories SET last_accessed_at = ?1, access_count = access_count + 1, strength = ?2 WHERE id = ?3",
287 params![now.to_rfc3339(), strength.clamp(0.0, 1.0), id],
288 )?;
289 Ok(())
290 }
291
292 pub fn get_memory(&self, id: i64) -> SqlResult<Option<MemoryRecord>> {
293 let mut stmt = self.conn.prepare(
294 "SELECT id, kind, subject, relation, object, episode_text,
295 strength, embedding, created_at, last_accessed_at, access_count,
296 tags, source, session_id, channel, importance, namespace, checksum
297 FROM memories WHERE id = ?1",
298 )?;
299 let mut rows = stmt.query_map(params![id], row_to_memory)?;
300 match rows.next() {
301 Some(r) => Ok(Some(r?)),
302 None => Ok(None),
303 }
304 }
305
306 pub fn decay_all(&self, decay_factor: f64, half_life_hours: f64) -> SqlResult<usize> {
309 self.decay_all_ns(decay_factor, half_life_hours, "default")
310 }
311
312 pub fn decay_all_ns(&self, decay_factor: f64, half_life_hours: f64, namespace: &str) -> SqlResult<usize> {
313 let now = Utc::now();
314 let mut stmt = self.conn.prepare(
315 "SELECT id, last_accessed_at, strength, importance FROM memories WHERE strength > 0.01 AND namespace = ?1",
316 )?;
317 let rows: Vec<(i64, String, f64, f64)> = stmt
318 .query_map(params![namespace], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get::<_, Option<f64>>(3)?.unwrap_or(0.5))))?
319 .collect::<SqlResult<Vec<_>>>()?;
320
321 let mut count = 0;
322 for (id, last_accessed, strength, importance) in &rows {
323 let last = DateTime::parse_from_rfc3339(last_accessed)
324 .unwrap_or_else(|_| now.into())
325 .with_timezone(&Utc);
326 let hours = (now - last).num_seconds() as f64 / 3600.0;
327 let effective_half_life = half_life_hours * (1.0 + importance);
330 let new_strength = (strength * decay_factor.powf(hours / effective_half_life)).max(0.0);
331 if (new_strength - strength).abs() > 1e-6 {
332 self.conn.execute("UPDATE memories SET strength = ?1 WHERE id = ?2", params![new_strength, id])?;
333 count += 1;
334 }
335 }
336 if count > 0 {
337 self.log_audit("decay", None, "system", Some(&format!("{{\"decayed\":{count},\"namespace\":{}}}", serde_json::json!(namespace))))?;
338 }
339 Ok(count)
340 }
341
342 pub fn forget_by_subject(&self, subject: &str) -> SqlResult<usize> {
345 self.forget_by_subject_ns(subject, "default")
346 }
347
348 pub fn forget_by_subject_ns(&self, subject: &str, namespace: &str) -> SqlResult<usize> {
349 let count = self.conn.execute("DELETE FROM memories WHERE subject = ?1 AND namespace = ?2", params![subject, namespace])?;
350 if count > 0 {
351 self.log_audit("forget", None, "system", Some(&format!("{{\"by\":\"subject\",\"subject\":{},\"count\":{},\"namespace\":{}}}", serde_json::json!(subject), count, serde_json::json!(namespace))))?;
352 }
353 Ok(count)
354 }
355
356 pub fn forget_by_id(&self, id: &str) -> SqlResult<usize> {
357 let changed = self.conn.execute("DELETE FROM memories WHERE id = ?1", params![id])?;
358 if changed > 0 {
359 self.log_audit("forget", Some(id.parse::<i64>().unwrap_or(0)), "system", Some(&format!("{{\"by\":\"id\",\"id\":{}}}", serde_json::json!(id))))?;
360 }
361 Ok(changed)
362 }
363
364 pub fn forget_older_than(&self, duration: Duration) -> SqlResult<usize> {
365 self.forget_older_than_ns(duration, "default")
366 }
367
368 pub fn forget_older_than_ns(&self, duration: Duration, namespace: &str) -> SqlResult<usize> {
369 let cutoff = (Utc::now() - duration).to_rfc3339();
370 let count = self.conn.execute("DELETE FROM memories WHERE created_at < ?1 AND namespace = ?2", params![cutoff, namespace])?;
371 if count > 0 {
372 self.log_audit("forget", None, "system", Some(&format!("{{\"by\":\"older_than\",\"count\":{},\"namespace\":{}}}", count, serde_json::json!(namespace))))?;
373 }
374 Ok(count)
375 }
376
377 pub fn memories_missing_embeddings(&self) -> SqlResult<Vec<MemoryRecord>> {
380 let mut stmt = self.conn.prepare(
381 "SELECT id, kind, subject, relation, object, episode_text,
382 strength, embedding, created_at, last_accessed_at, access_count,
383 tags, source, session_id, channel, importance, namespace, checksum
384 FROM memories WHERE embedding IS NULL",
385 )?;
386 let rows = stmt.query_map([], row_to_memory)?;
387 rows.collect()
388 }
389
390 pub fn update_embedding(&self, id: i64, embedding: &[f32]) -> SqlResult<()> {
391 self.conn.execute("UPDATE memories SET embedding = ?1 WHERE id = ?2", params![embedding_to_blob(embedding), id])?;
392 Ok(())
393 }
394
395 pub fn all_memories(&self) -> SqlResult<Vec<MemoryRecord>> {
398 let mut stmt = self.conn.prepare(
399 "SELECT id, kind, subject, relation, object, episode_text,
400 strength, embedding, created_at, last_accessed_at, access_count,
401 tags, source, session_id, channel, importance, namespace, checksum
402 FROM memories",
403 )?;
404 let rows = stmt.query_map([], row_to_memory)?;
405 rows.collect()
406 }
407
408 pub fn all_memories_ns(&self, namespace: &str) -> SqlResult<Vec<MemoryRecord>> {
409 let mut stmt = self.conn.prepare(
410 "SELECT id, kind, subject, relation, object, episode_text,
411 strength, embedding, created_at, last_accessed_at, access_count,
412 tags, source, session_id, channel, importance, namespace, checksum
413 FROM memories WHERE namespace = ?1",
414 )?;
415 let rows = stmt.query_map(params![namespace], row_to_memory)?;
416 rows.collect()
417 }
418
419 #[allow(clippy::too_many_arguments)]
420 pub fn import_fact(
421 &self, subject: &str, relation: &str, object: &str, strength: f64,
422 embedding: Option<&[f32]>, created_at: &str, last_accessed_at: &str, access_count: i64,
423 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
424 ) -> SqlResult<i64> {
425 self.import_fact_ns(subject, relation, object, strength, embedding, created_at, last_accessed_at, access_count, tags, source, session_id, channel, "default")
426 }
427
428 #[allow(clippy::too_many_arguments)]
429 pub fn import_fact_ns(
430 &self, subject: &str, relation: &str, object: &str, strength: f64,
431 embedding: Option<&[f32]>, created_at: &str, last_accessed_at: &str, access_count: i64,
432 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
433 namespace: &str,
434 ) -> SqlResult<i64> {
435 let emb_blob = embedding.map(embedding_to_blob);
436 let tags_str = tags.join(",");
437 let content = format!("{subject} {relation} {object}");
438 let checksum = compute_checksum(&content);
439 self.conn.execute(
440 "INSERT INTO memories (kind, subject, relation, object, strength, embedding, created_at, last_accessed_at, access_count, tags, source, session_id, channel, namespace, checksum)
441 VALUES ('fact', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
442 params![subject, relation, object, strength, emb_blob, created_at, last_accessed_at, access_count, tags_str, source, session_id, channel, namespace, checksum],
443 )?;
444 Ok(self.conn.last_insert_rowid())
445 }
446
447 #[allow(clippy::too_many_arguments)]
448 pub fn import_episode(
449 &self, text: &str, strength: f64, embedding: Option<&[f32]>,
450 created_at: &str, last_accessed_at: &str, access_count: i64,
451 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
452 ) -> SqlResult<i64> {
453 self.import_episode_ns(text, strength, embedding, created_at, last_accessed_at, access_count, tags, source, session_id, channel, "default")
454 }
455
456 #[allow(clippy::too_many_arguments)]
457 pub fn import_episode_ns(
458 &self, text: &str, strength: f64, embedding: Option<&[f32]>,
459 created_at: &str, last_accessed_at: &str, access_count: i64,
460 tags: &[String], source: Option<&str>, session_id: Option<&str>, channel: Option<&str>,
461 namespace: &str,
462 ) -> SqlResult<i64> {
463 let emb_blob = embedding.map(embedding_to_blob);
464 let tags_str = tags.join(",");
465 let checksum = compute_checksum(text);
466 self.conn.execute(
467 "INSERT INTO memories (kind, episode_text, strength, embedding, created_at, last_accessed_at, access_count, tags, source, session_id, channel, namespace, checksum)
468 VALUES ('episode', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
469 params![text, strength, emb_blob, created_at, last_accessed_at, access_count, tags_str, source, session_id, channel, namespace, checksum],
470 )?;
471 Ok(self.conn.last_insert_rowid())
472 }
473
474 pub fn update_tags(&self, id: i64, tags: &[String]) -> SqlResult<()> {
476 let tags_str = tags.join(",");
477 self.conn.execute(
478 "UPDATE memories SET tags = ?1 WHERE id = ?2",
479 params![tags_str, id],
480 )?;
481 Ok(())
482 }
483
484 pub fn update_importance(&self, id: i64, importance: f64) -> SqlResult<()> {
486 self.conn.execute(
487 "UPDATE memories SET importance = ?1 WHERE id = ?2",
488 params![importance.clamp(0.0, 1.0), id],
489 )?;
490 Ok(())
491 }
492
493 pub fn archive_memory(&self, id: i64) -> SqlResult<()> {
495 self.conn.execute(
496 "UPDATE memories SET strength = 0.0 WHERE id = ?1",
497 params![id],
498 )?;
499 Ok(())
500 }
501
502 pub fn delete_memory(&self, id: i64) -> SqlResult<()> {
504 self.conn.execute(
505 "DELETE FROM memories WHERE id = ?1",
506 params![id],
507 )?;
508 Ok(())
509 }
510
511 pub fn all_embeddings(&self) -> SqlResult<Vec<(i64, Vec<f32>)>> {
515 self.all_embeddings_ns("default")
516 }
517
518 pub fn all_embeddings_ns(&self, namespace: &str) -> SqlResult<Vec<(i64, Vec<f32>)>> {
519 let mut stmt = self.conn.prepare(
520 "SELECT id, embedding FROM memories WHERE embedding IS NOT NULL AND namespace = ?1",
521 )?;
522 let rows = stmt.query_map(params![namespace], |row| {
523 let id: i64 = row.get(0)?;
524 let blob: Vec<u8> = row.get(1)?;
525 Ok((id, blob_to_embedding(&blob)))
526 })?;
527 rows.collect()
528 }
529
530 pub fn reinforce_memory(&self, id: i64, boost: f64) -> SqlResult<()> {
532 let now = Utc::now().to_rfc3339();
533 self.conn.execute(
534 "UPDATE memories SET strength = MIN(strength + ?1, 1.0), \
535 last_accessed_at = ?2, access_count = access_count + 1 WHERE id = ?3",
536 params![boost, now, id],
537 )?;
538 self.log_audit("reinforce", Some(id), "system", Some(&format!("{{\"boost\":{boost}}}")))
539 }
540
541 pub fn facts_involving(&self, entity: &str) -> SqlResult<Vec<MemoryRecord>> {
545 let mut stmt = self.conn.prepare(
546 "SELECT id, kind, subject, relation, object, episode_text,
547 strength, embedding, created_at, last_accessed_at, access_count,
548 tags, source, session_id, channel, importance, namespace, checksum
549 FROM memories WHERE kind = 'fact' AND (subject = ?1 OR object = ?1)",
550 )?;
551 let rows = stmt.query_map(params![entity], row_to_memory)?;
552 rows.collect()
553 }
554
555 pub fn stats(&self) -> SqlResult<MemoryStats> {
558 self.stats_ns("default")
559 }
560
561 pub fn stats_ns(&self, namespace: &str) -> SqlResult<MemoryStats> {
562 let total_memories: i64 = self.conn.query_row("SELECT COUNT(*) FROM memories WHERE namespace = ?1", params![namespace], |r| r.get(0))?;
563 let total_facts: i64 = self.conn.query_row("SELECT COUNT(*) FROM memories WHERE kind = 'fact' AND namespace = ?1", params![namespace], |r| r.get(0))?;
564 let total_episodes: i64 = self.conn.query_row("SELECT COUNT(*) FROM memories WHERE kind = 'episode' AND namespace = ?1", params![namespace], |r| r.get(0))?;
565 let avg_strength: f64 = self.conn.query_row("SELECT COALESCE(AVG(strength), 0.0) FROM memories WHERE namespace = ?1", params![namespace], |r| r.get(0))?;
566 Ok(MemoryStats { total_memories, total_facts, total_episodes, avg_strength })
567 }
568 pub fn log_audit(&self, action: &str, memory_id: Option<i64>, actor: &str, details_json: Option<&str>) -> SqlResult<()> {
571 let now = Utc::now().to_rfc3339();
572 self.conn.execute(
573 "INSERT INTO audit_log (timestamp, action, memory_id, actor, details_json)
574 VALUES (?1, ?2, ?3, ?4, ?5)",
575 params![now, action, memory_id, actor, details_json],
576 )?;
577 Ok(())
578 }
579
580 pub fn get_audit_log(&self, limit: usize, memory_id: Option<i64>, actor: Option<&str>) -> SqlResult<Vec<AuditEntry>> {
581 let (sql, param_values) = build_audit_query(limit, memory_id, actor);
582 let mut stmt = self.conn.prepare(&sql)?;
583 let rows = stmt.query_map(rusqlite::params_from_iter(param_values.iter()), |row| {
584 Ok(AuditEntry {
585 id: row.get(0)?,
586 timestamp: parse_datetime(&row.get::<_, String>(1)?),
587 action: row.get(2)?,
588 memory_id: row.get(3)?,
589 actor: row.get(4)?,
590 details_json: row.get(5)?,
591 })
592 })?;
593 rows.collect()
594 }
595
596 pub fn verify_integrity(&self) -> SqlResult<VerifyResult> {
599 self.verify_integrity_ns("default")
600 }
601
602 pub fn verify_integrity_ns(&self, namespace: &str) -> SqlResult<VerifyResult> {
603 let mut stmt = self.conn.prepare(
604 "SELECT id, kind, subject, relation, object, episode_text, checksum
605 FROM memories WHERE namespace = ?1",
606 )?;
607 let rows: Vec<(i64, String, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)> = stmt
608 .query_map(params![namespace], |row| {
609 Ok((
610 row.get(0)?, row.get(1)?, row.get(2)?,
611 row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?,
612 ))
613 })?
614 .collect::<SqlResult<Vec<_>>>()?;
615
616 let mut total_checked = 0;
617 let mut valid = 0;
618 let mut corrupted = Vec::new();
619 let mut missing_checksum = 0;
620
621 for (id, kind, subject, relation, object, episode_text, stored_checksum) in rows {
622 total_checked += 1;
623 let content = match kind.as_str() {
624 "fact" => format!("{} {} {}",
625 subject.as_deref().unwrap_or(""),
626 relation.as_deref().unwrap_or(""),
627 object.as_deref().unwrap_or("")),
628 _ => episode_text.unwrap_or_default(),
629 };
630 let actual_checksum = compute_checksum(&content);
631 match stored_checksum {
632 Some(expected) => {
633 if expected == actual_checksum {
634 valid += 1;
635 } else {
636 corrupted.push(CorruptedMemory {
637 id,
638 expected,
639 actual: actual_checksum,
640 });
641 }
642 }
643 None => {
644 missing_checksum += 1;
645 }
646 }
647 }
648
649 Ok(VerifyResult { total_checked, valid, corrupted, missing_checksum })
650 }
651}
652
653fn compute_checksum(content: &str) -> String {
656 let mut hasher = Sha256::new();
657 hasher.update(content.as_bytes());
658 format!("{:x}", hasher.finalize())
659}
660
661pub fn content_checksum(content: &str) -> String {
663 compute_checksum(content)
664}
665
666fn build_audit_query(limit: usize, memory_id: Option<i64>, actor: Option<&str>) -> (String, Vec<String>) {
667 let mut conditions = Vec::new();
668 let mut param_values: Vec<String> = Vec::new();
669
670 if let Some(mid) = memory_id {
671 param_values.push(mid.to_string());
672 conditions.push(format!("memory_id = ?{}", param_values.len()));
673 }
674 if let Some(a) = actor {
675 param_values.push(a.to_string());
676 conditions.push(format!("actor = ?{}", param_values.len()));
677 }
678
679 let where_clause = if conditions.is_empty() {
680 String::new()
681 } else {
682 format!(" WHERE {}", conditions.join(" AND "))
683 };
684
685 param_values.push(limit.to_string());
686 let sql = format!(
687 "SELECT id, timestamp, action, memory_id, actor, details_json FROM audit_log{} ORDER BY id DESC LIMIT ?{}",
688 where_clause, param_values.len()
689 );
690 (sql, param_values)
691}
692
693fn embedding_to_blob(emb: &[f32]) -> Vec<u8> {
694 emb.iter().flat_map(|f| f.to_le_bytes()).collect()
695}
696
697fn blob_to_embedding(blob: &[u8]) -> Vec<f32> {
698 blob.chunks_exact(4)
699 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
700 .collect()
701}
702
703fn parse_datetime(s: &str) -> DateTime<Utc> {
704 DateTime::parse_from_rfc3339(s)
705 .map(|dt| dt.with_timezone(&Utc))
706 .unwrap_or_else(|_| Utc::now())
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 #[test]
714 fn remember_fact_with_tags_stores_and_retrieves() {
715 let store = MemoryStore::open_in_memory().unwrap();
716 let tags = vec!["preference".to_string(), "technical".to_string()];
717 let id = store.remember_fact_with_tags("Rust", "is", "fast", None, &tags).unwrap();
718 let mem = store.get_memory(id).unwrap().unwrap();
719 assert_eq!(mem.tags, vec!["preference", "technical"]);
720 }
721
722 #[test]
723 fn remember_episode_with_tags_stores_and_retrieves() {
724 let store = MemoryStore::open_in_memory().unwrap();
725 let tags = vec!["decision".to_string()];
726 let id = store.remember_episode_with_tags("chose Rust over Go", None, &tags).unwrap();
727 let mem = store.get_memory(id).unwrap().unwrap();
728 assert_eq!(mem.tags, vec!["decision"]);
729 }
730
731 #[test]
732 fn remember_without_tags_returns_empty_vec() {
733 let store = MemoryStore::open_in_memory().unwrap();
734 let id = store.remember_fact("Jared", "likes", "pizza", None).unwrap();
735 let mem = store.get_memory(id).unwrap().unwrap();
736 assert!(mem.tags.is_empty());
737 }
738
739 #[test]
740 fn update_tags_modifies_existing_memory() {
741 let store = MemoryStore::open_in_memory().unwrap();
742 let id = store.remember_fact("Jared", "uses", "Linux", None).unwrap();
743 let mem = store.get_memory(id).unwrap().unwrap();
744 assert!(mem.tags.is_empty());
745
746 store.update_tags(id, &["technical".to_string(), "preference".to_string()]).unwrap();
747 let mem = store.get_memory(id).unwrap().unwrap();
748 assert_eq!(mem.tags, vec!["technical", "preference"]);
749 }
750
751 #[test]
752 fn tags_survive_export_import_roundtrip() {
753 let store = MemoryStore::open_in_memory().unwrap();
754 let tags = vec!["project".to_string(), "meta".to_string()];
755 store.remember_fact_with_tags("Conch", "is_a", "memory system", None, &tags).unwrap();
756
757 let all = store.all_memories().unwrap();
758 assert_eq!(all.len(), 1);
759 assert_eq!(all[0].tags, vec!["project", "meta"]);
760
761 let store2 = MemoryStore::open_in_memory().unwrap();
763 let mem = &all[0];
764 let created = mem.created_at.to_rfc3339();
765 let accessed = mem.last_accessed_at.to_rfc3339();
766 if let MemoryKind::Fact(f) = &mem.kind {
767 store2.import_fact(&f.subject, &f.relation, &f.object, mem.strength, None, &created, &accessed, mem.access_count, &mem.tags, None, None, None).unwrap();
768 }
769 let imported = store2.all_memories().unwrap();
770 assert_eq!(imported.len(), 1);
771 assert_eq!(imported[0].tags, vec!["project", "meta"]);
772 }
773
774 #[test]
775 fn tags_appear_in_all_memories_with_text() {
776 let store = MemoryStore::open_in_memory().unwrap();
777 let tags = vec!["person".to_string()];
778 store.remember_fact_with_tags("Jared", "is", "developer", None, &tags).unwrap();
779 let results = store.all_memories_with_text().unwrap();
780 assert_eq!(results.len(), 1);
781 assert_eq!(results[0].0.tags, vec!["person"]);
782 }
783
784 #[test]
785 fn tag_filtered_recall_returns_only_matching() {
786 let store = MemoryStore::open_in_memory().unwrap();
787 store.remember_fact_with_tags("A", "is", "tagged", None, &["technical".to_string()]).unwrap();
788 store.remember_fact("B", "is", "untagged", None).unwrap();
789 store.remember_fact_with_tags("C", "is", "also-tagged", None, &["technical".to_string(), "project".to_string()]).unwrap();
790
791 let results = store.all_memories_with_text_filtered_by_tag("technical").unwrap();
792 assert_eq!(results.len(), 2);
793 for (m, _) in &results {
794 assert!(m.tags.contains(&"technical".to_string()));
795 }
796 }
797
798 #[test]
799 fn tag_filter_exact_match_not_substring() {
800 let store = MemoryStore::open_in_memory().unwrap();
801 store.remember_fact_with_tags("A", "is", "tech", None, &["technical".to_string()]).unwrap();
802 store.remember_fact_with_tags("B", "is", "meta", None, &["meta".to_string()]).unwrap();
803
804 let results = store.all_memories_with_text_filtered_by_tag("tech").unwrap();
806 assert_eq!(results.len(), 0);
807
808 let results = store.all_memories_with_text_filtered_by_tag("meta").unwrap();
810 assert_eq!(results.len(), 1);
811 }
812
813 #[test]
814 fn tag_filter_returns_empty_when_no_match() {
815 let store = MemoryStore::open_in_memory().unwrap();
816 store.remember_fact_with_tags("A", "is", "B", None, &["technical".to_string()]).unwrap();
817
818 let results = store.all_memories_with_text_filtered_by_tag("nonexistent").unwrap();
819 assert!(results.is_empty());
820 }
821
822 #[test]
825 fn facts_involving_finds_subject_and_object() {
826 let store = MemoryStore::open_in_memory().unwrap();
827 store.remember_fact("Alice", "knows", "Bob", None).unwrap();
828 store.remember_fact("Bob", "works_at", "Acme", None).unwrap();
829 store.remember_fact("Charlie", "knows", "Alice", None).unwrap();
830
831 let results = store.facts_involving("Alice").unwrap();
833 assert_eq!(results.len(), 2, "Alice should appear in 2 facts");
834
835 let results = store.facts_involving("Bob").unwrap();
837 assert_eq!(results.len(), 2, "Bob should appear in 2 facts");
838
839 let results = store.facts_involving("Acme").unwrap();
841 assert_eq!(results.len(), 1, "Acme should appear in 1 fact");
842 }
843
844 #[test]
845 fn facts_involving_ignores_episodes() {
846 let store = MemoryStore::open_in_memory().unwrap();
847 store.remember_fact("Alice", "knows", "Bob", None).unwrap();
848 store.remember_episode("Alice had a meeting", None).unwrap();
849
850 let results = store.facts_involving("Alice").unwrap();
851 assert_eq!(results.len(), 1, "should only return facts, not episodes");
852 }
853
854 #[test]
855 fn facts_involving_returns_empty_for_unknown() {
856 let store = MemoryStore::open_in_memory().unwrap();
857 store.remember_fact("Alice", "knows", "Bob", None).unwrap();
858
859 let results = store.facts_involving("Unknown").unwrap();
860 assert!(results.is_empty());
861 }
862
863 #[test]
866 fn remember_fact_full_stores_source_fields() {
867 let store = MemoryStore::open_in_memory().unwrap();
868 let id = store.remember_fact_full(
869 "Jared", "uses", "conch", None, &[],
870 Some("cli"), Some("session-123"), Some("#general"),
871 ).unwrap();
872 let mem = store.get_memory(id).unwrap().unwrap();
873 assert_eq!(mem.source.as_deref(), Some("cli"));
874 assert_eq!(mem.session_id.as_deref(), Some("session-123"));
875 assert_eq!(mem.channel.as_deref(), Some("#general"));
876 }
877
878 #[test]
879 fn remember_episode_full_stores_source_fields() {
880 let store = MemoryStore::open_in_memory().unwrap();
881 let id = store.remember_episode_full(
882 "had a meeting", None, &[],
883 Some("discord"), Some("sess-abc"), Some("#dev"),
884 ).unwrap();
885 let mem = store.get_memory(id).unwrap().unwrap();
886 assert_eq!(mem.source.as_deref(), Some("discord"));
887 assert_eq!(mem.session_id.as_deref(), Some("sess-abc"));
888 assert_eq!(mem.channel.as_deref(), Some("#dev"));
889 }
890
891 #[test]
892 fn remember_without_source_returns_none() {
893 let store = MemoryStore::open_in_memory().unwrap();
894 let id = store.remember_fact("X", "Y", "Z", None).unwrap();
895 let mem = store.get_memory(id).unwrap().unwrap();
896 assert!(mem.source.is_none());
897 assert!(mem.session_id.is_none());
898 assert!(mem.channel.is_none());
899 }
900
901 #[test]
902 fn source_fields_survive_export_import_roundtrip() {
903 let store = MemoryStore::open_in_memory().unwrap();
904 store.remember_fact_full(
905 "Conch", "source_test", "value", None, &["meta".to_string()],
906 Some("cron"), Some("daily-job"), None,
907 ).unwrap();
908
909 let all = store.all_memories().unwrap();
910 assert_eq!(all.len(), 1);
911 assert_eq!(all[0].source.as_deref(), Some("cron"));
912 assert_eq!(all[0].session_id.as_deref(), Some("daily-job"));
913 assert!(all[0].channel.is_none());
914
915 let store2 = MemoryStore::open_in_memory().unwrap();
917 let mem = &all[0];
918 let created = mem.created_at.to_rfc3339();
919 let accessed = mem.last_accessed_at.to_rfc3339();
920 if let MemoryKind::Fact(f) = &mem.kind {
921 store2.import_fact(
922 &f.subject, &f.relation, &f.object, mem.strength, None,
923 &created, &accessed, mem.access_count, &mem.tags,
924 mem.source.as_deref(), mem.session_id.as_deref(), mem.channel.as_deref(),
925 ).unwrap();
926 }
927 let imported = store2.all_memories().unwrap();
928 assert_eq!(imported.len(), 1);
929 assert_eq!(imported[0].source.as_deref(), Some("cron"));
930 assert_eq!(imported[0].session_id.as_deref(), Some("daily-job"));
931 assert!(imported[0].channel.is_none());
932 }
933
934 #[test]
935 fn source_fields_appear_in_all_memories_with_text() {
936 let store = MemoryStore::open_in_memory().unwrap();
937 store.remember_fact_full(
938 "test", "source", "recall", None, &[],
939 Some("mcp"), None, Some("#test-channel"),
940 ).unwrap();
941 let results = store.all_memories_with_text().unwrap();
942 assert_eq!(results.len(), 1);
943 assert_eq!(results[0].0.source.as_deref(), Some("mcp"));
944 assert!(results[0].0.session_id.is_none());
945 assert_eq!(results[0].0.channel.as_deref(), Some("#test-channel"));
946 }
947
948 #[test]
951 fn upsert_fact_inserts_when_no_match() {
952 let store = MemoryStore::open_in_memory().unwrap();
953 let (id, was_updated) = store.upsert_fact("Jared", "favorite_color", "blue", None, &[], None, None, None).unwrap();
954 assert!(!was_updated);
955 let mem = store.get_memory(id).unwrap().unwrap();
956 if let MemoryKind::Fact(f) = &mem.kind {
957 assert_eq!(f.object, "blue");
958 } else { panic!("expected fact"); }
959 }
960
961 #[test]
962 fn upsert_fact_updates_existing_object() {
963 let store = MemoryStore::open_in_memory().unwrap();
964 let (id1, _) = store.upsert_fact("Jared", "favorite_color", "blue", None, &[], None, None, None).unwrap();
965 let (id2, was_updated) = store.upsert_fact("Jared", "favorite_color", "green", None, &[], None, None, None).unwrap();
966 assert!(was_updated);
967 assert_eq!(id1, id2, "should update the same row");
968
969 let mem = store.get_memory(id2).unwrap().unwrap();
970 if let MemoryKind::Fact(f) = &mem.kind {
971 assert_eq!(f.object, "green", "object should be updated to green");
972 } else { panic!("expected fact"); }
973
974 let stats = store.stats().unwrap();
976 assert_eq!(stats.total_memories, 1);
977 }
978
979 #[test]
980 fn upsert_fact_bumps_access_count() {
981 let store = MemoryStore::open_in_memory().unwrap();
982 store.upsert_fact("Jared", "age", "30", None, &[], None, None, None).unwrap();
983 let (id, _) = store.upsert_fact("Jared", "age", "31", None, &[], None, None, None).unwrap();
984 let mem = store.get_memory(id).unwrap().unwrap();
985 assert_eq!(mem.access_count, 1, "access count should be bumped on upsert");
986 }
987
988 #[test]
989 fn upsert_fact_different_relation_creates_new() {
990 let store = MemoryStore::open_in_memory().unwrap();
991 let (id1, _) = store.upsert_fact("Jared", "likes", "Rust", None, &[], None, None, None).unwrap();
992 let (id2, was_updated) = store.upsert_fact("Jared", "uses", "Rust", None, &[], None, None, None).unwrap();
993 assert!(!was_updated, "different relation should insert new fact");
994 assert_ne!(id1, id2);
995 assert_eq!(store.stats().unwrap().total_memories, 2);
996 }
997
998 #[test]
999 fn upsert_fact_preserves_tags() {
1000 let store = MemoryStore::open_in_memory().unwrap();
1001 store.upsert_fact("Jared", "color", "blue", None, &["preference".to_string()], None, None, None).unwrap();
1002 let (id, _) = store.upsert_fact("Jared", "color", "green", None, &["preference".to_string(), "updated".to_string()], None, None, None).unwrap();
1003 let mem = store.get_memory(id).unwrap().unwrap();
1004 assert_eq!(mem.tags, vec!["preference", "updated"]);
1005 }
1006
1007 #[test]
1014 fn audit_log_records_remember_fact() {
1015 let store = MemoryStore::open_in_memory().unwrap();
1016 store.remember_fact("Jared", "likes", "Rust", None).unwrap();
1017
1018 let log = store.get_audit_log(10, None, None).unwrap();
1019 assert!(!log.is_empty());
1020 let remember_entries: Vec<_> = log.iter().filter(|e| e.action == "remember").collect();
1021 assert_eq!(remember_entries.len(), 1);
1022 assert!(remember_entries[0].memory_id.is_some());
1023 assert_eq!(remember_entries[0].actor, "system");
1024 }
1025
1026 #[test]
1027 fn audit_log_records_remember_episode() {
1028 let store = MemoryStore::open_in_memory().unwrap();
1029 store.remember_episode("had coffee", None).unwrap();
1030
1031 let log = store.get_audit_log(10, None, None).unwrap();
1032 let remember_entries: Vec<_> = log.iter().filter(|e| e.action == "remember").collect();
1033 assert_eq!(remember_entries.len(), 1);
1034 }
1035
1036 #[test]
1037 fn audit_log_records_forget_by_id() {
1038 let store = MemoryStore::open_in_memory().unwrap();
1039 let id = store.remember_fact("X", "Y", "Z", None).unwrap();
1040 store.forget_by_id(&id.to_string()).unwrap();
1041
1042 let log = store.get_audit_log(10, None, None).unwrap();
1043 assert!(log.iter().any(|e| e.action == "forget"));
1044 }
1045
1046 #[test]
1047 fn audit_log_records_forget_by_subject() {
1048 let store = MemoryStore::open_in_memory().unwrap();
1049 store.remember_fact("Jared", "likes", "Rust", None).unwrap();
1050 store.forget_by_subject("Jared").unwrap();
1051
1052 let log = store.get_audit_log(10, None, None).unwrap();
1053 assert!(log.iter().any(|e| e.action == "forget"));
1054 }
1055
1056 #[test]
1057 fn audit_log_records_upsert_update() {
1058 let store = MemoryStore::open_in_memory().unwrap();
1059 store.upsert_fact("Jared", "color", "blue", None, &[], None, None, None).unwrap();
1060 store.upsert_fact("Jared", "color", "green", None, &[], None, None, None).unwrap();
1061
1062 let log = store.get_audit_log(10, None, None).unwrap();
1063 assert!(log.iter().any(|e| e.action == "update"));
1064 }
1065
1066 #[test]
1067 fn audit_log_records_reinforce() {
1068 let store = MemoryStore::open_in_memory().unwrap();
1069 let id = store.remember_fact("A", "B", "C", None).unwrap();
1070 store.reinforce_memory(id, 0.1).unwrap();
1071
1072 let log = store.get_audit_log(10, None, None).unwrap();
1073 assert!(log.iter().any(|e| e.action == "reinforce"));
1074 }
1075
1076 #[test]
1077 fn audit_log_records_decay() {
1078 let store = MemoryStore::open_in_memory().unwrap();
1079 store.remember_fact("A", "B", "C", None).unwrap();
1080
1081 let old_time = (Utc::now() - chrono::Duration::hours(48)).to_rfc3339();
1083 store.conn().execute("UPDATE memories SET last_accessed_at = ?1", params![old_time]).unwrap();
1084
1085 store.decay_all(0.5, 24.0).unwrap();
1086 let log = store.get_audit_log(10, None, None).unwrap();
1087 assert!(log.iter().any(|e| e.action == "decay"), "should log decay action");
1088 }
1089
1090 #[test]
1091 fn audit_log_filter_by_memory_id() {
1092 let store = MemoryStore::open_in_memory().unwrap();
1093 let id1 = store.remember_fact("A", "B", "C", None).unwrap();
1094 store.remember_fact("D", "E", "F", None).unwrap();
1095
1096 let log = store.get_audit_log(10, Some(id1), None).unwrap();
1097 for entry in &log {
1098 assert_eq!(entry.memory_id, Some(id1));
1099 }
1100 }
1101
1102 #[test]
1103 fn audit_log_filter_by_actor() {
1104 let store = MemoryStore::open_in_memory().unwrap();
1105 store.remember_fact("A", "B", "C", None).unwrap();
1106 store.log_audit("custom_action", None, "agent-x", None).unwrap();
1107
1108 let log = store.get_audit_log(10, None, Some("agent-x")).unwrap();
1109 assert_eq!(log.len(), 1);
1110 assert_eq!(log[0].actor, "agent-x");
1111 }
1112
1113 #[test]
1114 fn audit_log_limit_works() {
1115 let store = MemoryStore::open_in_memory().unwrap();
1116 for i in 0..10 {
1117 store.remember_fact(&format!("S{i}"), "R", "O", None).unwrap();
1118 }
1119 let log = store.get_audit_log(3, None, None).unwrap();
1120 assert_eq!(log.len(), 3);
1121 }
1122
1123 #[test]
1124 fn audit_log_has_details_json() {
1125 let store = MemoryStore::open_in_memory().unwrap();
1126 store.remember_fact("Jared", "likes", "Rust", None).unwrap();
1127
1128 let log = store.get_audit_log(1, None, None).unwrap();
1129 assert!(log[0].details_json.is_some());
1130 let details = log[0].details_json.as_ref().unwrap();
1131 assert!(details.contains("\"kind\":\"fact\""));
1132 assert!(details.contains("\"subject\":\"Jared\""));
1133 }
1134
1135 #[test]
1138 fn fact_gets_checksum_on_insert() {
1139 let store = MemoryStore::open_in_memory().unwrap();
1140 let id = store.remember_fact("Jared", "likes", "Rust", None).unwrap();
1141 let mem = store.get_memory(id).unwrap().unwrap();
1142 assert!(mem.checksum.is_some());
1143 assert!(!mem.checksum.as_ref().unwrap().is_empty());
1144 }
1145
1146 #[test]
1147 fn episode_gets_checksum_on_insert() {
1148 let store = MemoryStore::open_in_memory().unwrap();
1149 let id = store.remember_episode("had coffee", None).unwrap();
1150 let mem = store.get_memory(id).unwrap().unwrap();
1151 assert!(mem.checksum.is_some());
1152 }
1153
1154 #[test]
1155 fn checksum_is_consistent_for_same_content() {
1156 let store = MemoryStore::open_in_memory().unwrap();
1157 let id1 = store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "ns1").unwrap();
1158 let id2 = store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "ns2").unwrap();
1159 let mem1 = store.get_memory(id1).unwrap().unwrap();
1160 let mem2 = store.get_memory(id2).unwrap().unwrap();
1161 assert_eq!(mem1.checksum, mem2.checksum, "same content should produce same checksum");
1162 }
1163
1164 #[test]
1165 fn checksum_differs_for_different_content() {
1166 let store = MemoryStore::open_in_memory().unwrap();
1167 let id1 = store.remember_fact("A", "B", "C", None).unwrap();
1168 let id2 = store.remember_fact("X", "Y", "Z", None).unwrap();
1169 let mem1 = store.get_memory(id1).unwrap().unwrap();
1170 let mem2 = store.get_memory(id2).unwrap().unwrap();
1171 assert_ne!(mem1.checksum, mem2.checksum);
1172 }
1173
1174 #[test]
1175 fn upsert_updates_checksum() {
1176 let store = MemoryStore::open_in_memory().unwrap();
1177 let (id, _) = store.upsert_fact("Jared", "color", "blue", None, &[], None, None, None).unwrap();
1178 let mem1 = store.get_memory(id).unwrap().unwrap();
1179 let checksum1 = mem1.checksum.clone().unwrap();
1180
1181 store.upsert_fact("Jared", "color", "green", None, &[], None, None, None).unwrap();
1182 let mem2 = store.get_memory(id).unwrap().unwrap();
1183 let checksum2 = mem2.checksum.clone().unwrap();
1184 assert_ne!(checksum1, checksum2, "upsert with new object should change checksum");
1185 }
1186
1187 #[test]
1190 fn verify_all_valid() {
1191 let store = MemoryStore::open_in_memory().unwrap();
1192 store.remember_fact("A", "B", "C", None).unwrap();
1193 store.remember_episode("hello", None).unwrap();
1194
1195 let result = store.verify_integrity().unwrap();
1196 assert_eq!(result.total_checked, 2);
1197 assert_eq!(result.valid, 2);
1198 assert!(result.corrupted.is_empty());
1199 assert_eq!(result.missing_checksum, 0);
1200 }
1201
1202 #[test]
1203 fn verify_detects_corrupted_fact() {
1204 let store = MemoryStore::open_in_memory().unwrap();
1205 let id = store.remember_fact("Jared", "likes", "Rust", None).unwrap();
1206
1207 store.conn().execute("UPDATE memories SET object = 'Go' WHERE id = ?1", params![id]).unwrap();
1209
1210 let result = store.verify_integrity().unwrap();
1211 assert_eq!(result.corrupted.len(), 1);
1212 assert_eq!(result.corrupted[0].id, id);
1213 }
1214
1215 #[test]
1216 fn verify_detects_corrupted_episode() {
1217 let store = MemoryStore::open_in_memory().unwrap();
1218 let id = store.remember_episode("original text", None).unwrap();
1219
1220 store.conn().execute("UPDATE memories SET episode_text = 'modified text' WHERE id = ?1", params![id]).unwrap();
1222
1223 let result = store.verify_integrity().unwrap();
1224 assert_eq!(result.corrupted.len(), 1);
1225 assert_eq!(result.corrupted[0].id, id);
1226 }
1227
1228 #[test]
1229 fn verify_reports_missing_checksums() {
1230 let store = MemoryStore::open_in_memory().unwrap();
1231 store.remember_fact("A", "B", "C", None).unwrap();
1232
1233 store.conn().execute("UPDATE memories SET checksum = NULL", []).unwrap();
1235
1236 let result = store.verify_integrity().unwrap();
1237 assert_eq!(result.missing_checksum, 1);
1238 assert_eq!(result.valid, 0);
1239 assert!(result.corrupted.is_empty());
1240 }
1241
1242 #[test]
1243 fn verify_namespace_scoped() {
1244 let store = MemoryStore::open_in_memory().unwrap();
1245 store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "ns-a").unwrap();
1246 store.remember_fact_ns("X", "Y", "Z", None, &[], None, None, None, "ns-b").unwrap();
1247
1248 store.conn().execute("UPDATE memories SET object = 'CORRUPTED' WHERE namespace = 'ns-b'", []).unwrap();
1250
1251 let result_a = store.verify_integrity_ns("ns-a").unwrap();
1252 let result_b = store.verify_integrity_ns("ns-b").unwrap();
1253
1254 assert_eq!(result_a.valid, 1);
1255 assert!(result_a.corrupted.is_empty(), "ns-a should be clean");
1256 assert_eq!(result_b.corrupted.len(), 1, "ns-b should have corruption");
1257 }
1258
1259 #[test]
1262 fn namespace_default_on_remember() {
1263 let store = MemoryStore::open_in_memory().unwrap();
1264 let id = store.remember_fact("A", "B", "C", None).unwrap();
1265 let mem = store.get_memory(id).unwrap().unwrap();
1266 assert_eq!(mem.namespace, "default");
1267 }
1268
1269 #[test]
1270 fn namespace_set_on_remember_ns() {
1271 let store = MemoryStore::open_in_memory().unwrap();
1272 let id = store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "project-x").unwrap();
1273 let mem = store.get_memory(id).unwrap().unwrap();
1274 assert_eq!(mem.namespace, "project-x");
1275 }
1276
1277 #[test]
1278 fn namespace_isolates_all_memories_with_text() {
1279 let store = MemoryStore::open_in_memory().unwrap();
1280 store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "ns1").unwrap();
1281 store.remember_fact_ns("X", "Y", "Z", None, &[], None, None, None, "ns2").unwrap();
1282
1283 let ns1 = store.all_memories_with_text_ns("ns1").unwrap();
1284 let ns2 = store.all_memories_with_text_ns("ns2").unwrap();
1285 assert_eq!(ns1.len(), 1);
1286 assert_eq!(ns2.len(), 1);
1287 assert_ne!(ns1[0].0.id, ns2[0].0.id);
1288 }
1289
1290 #[test]
1291 fn namespace_isolates_stats() {
1292 let store = MemoryStore::open_in_memory().unwrap();
1293 store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "ns1").unwrap();
1294 store.remember_fact_ns("D", "E", "F", None, &[], None, None, None, "ns1").unwrap();
1295 store.remember_fact_ns("X", "Y", "Z", None, &[], None, None, None, "ns2").unwrap();
1296
1297 let stats1 = store.stats_ns("ns1").unwrap();
1298 let stats2 = store.stats_ns("ns2").unwrap();
1299 assert_eq!(stats1.total_memories, 2);
1300 assert_eq!(stats2.total_memories, 1);
1301 }
1302
1303 #[test]
1304 fn namespace_isolates_upsert() {
1305 let store = MemoryStore::open_in_memory().unwrap();
1306 store.upsert_fact_ns("Jared", "color", "blue", None, &[], None, None, None, "ns1").unwrap();
1307 store.upsert_fact_ns("Jared", "color", "red", None, &[], None, None, None, "ns2").unwrap();
1308
1309 let (_, updated) = store.upsert_fact_ns("Jared", "color", "green", None, &[], None, None, None, "ns1").unwrap();
1311 assert!(updated);
1312
1313 let ns2_mems = store.all_memories_ns("ns2").unwrap();
1314 if let MemoryKind::Fact(f) = &ns2_mems[0].kind {
1315 assert_eq!(f.object, "red", "ns2 should be unaffected");
1316 } else { panic!("expected fact"); }
1317 }
1318
1319 #[test]
1320 fn namespace_isolates_forget_by_subject() {
1321 let store = MemoryStore::open_in_memory().unwrap();
1322 store.remember_fact_ns("Jared", "likes", "A", None, &[], None, None, None, "ns1").unwrap();
1323 store.remember_fact_ns("Jared", "likes", "B", None, &[], None, None, None, "ns2").unwrap();
1324
1325 let deleted = store.forget_by_subject_ns("Jared", "ns1").unwrap();
1326 assert_eq!(deleted, 1);
1327
1328 assert_eq!(store.stats_ns("ns2").unwrap().total_memories, 1);
1330 assert_eq!(store.stats_ns("ns1").unwrap().total_memories, 0);
1331 }
1332
1333 #[test]
1334 fn namespace_isolates_decay() {
1335 let store = MemoryStore::open_in_memory().unwrap();
1336 store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "ns1").unwrap();
1337 store.remember_fact_ns("X", "Y", "Z", None, &[], None, None, None, "ns2").unwrap();
1338
1339 let old_time = (Utc::now() - chrono::Duration::hours(48)).to_rfc3339();
1341 store.conn().execute("UPDATE memories SET last_accessed_at = ?1", params![old_time]).unwrap();
1342
1343 let decayed = store.decay_all_ns(0.5, 24.0, "ns1").unwrap();
1344 assert_eq!(decayed, 1, "should only decay ns1 memories");
1345
1346 let ns2_mems = store.all_memories_ns("ns2").unwrap();
1348 assert!((ns2_mems[0].strength - 1.0).abs() < 0.01, "ns2 should not be decayed");
1349 }
1350
1351 #[test]
1352 fn namespace_isolates_embeddings() {
1353 let store = MemoryStore::open_in_memory().unwrap();
1354 store.remember_fact_ns("A", "B", "C", Some(&[1.0, 0.0]), &[], None, None, None, "ns1").unwrap();
1355 store.remember_fact_ns("X", "Y", "Z", Some(&[0.0, 1.0]), &[], None, None, None, "ns2").unwrap();
1356
1357 let emb1 = store.all_embeddings_ns("ns1").unwrap();
1358 let emb2 = store.all_embeddings_ns("ns2").unwrap();
1359 assert_eq!(emb1.len(), 1);
1360 assert_eq!(emb2.len(), 1);
1361 }
1362
1363 #[test]
1364 fn namespace_episode_isolation() {
1365 let store = MemoryStore::open_in_memory().unwrap();
1366 store.remember_episode_ns("event in ns1", None, &[], None, None, None, "ns1").unwrap();
1367 store.remember_episode_ns("event in ns2", None, &[], None, None, None, "ns2").unwrap();
1368
1369 let ns1 = store.all_memories_with_text_ns("ns1").unwrap();
1370 let ns2 = store.all_memories_with_text_ns("ns2").unwrap();
1371 assert_eq!(ns1.len(), 1);
1372 assert_eq!(ns2.len(), 1);
1373 assert!(ns1[0].1.contains("ns1"));
1374 assert!(ns2[0].1.contains("ns2"));
1375 }
1376
1377 #[test]
1378 fn namespace_import_export_scoped() {
1379 let store = MemoryStore::open_in_memory().unwrap();
1380 store.remember_fact_ns("A", "B", "C", None, &[], None, None, None, "ns1").unwrap();
1381 store.remember_fact_ns("X", "Y", "Z", None, &[], None, None, None, "ns2").unwrap();
1382
1383 let ns1_mems = store.all_memories_ns("ns1").unwrap();
1384 assert_eq!(ns1_mems.len(), 1);
1385 assert_eq!(ns1_mems[0].namespace, "ns1");
1386 }
1387}
1388
1389fn row_to_memory(row: &rusqlite::Row) -> SqlResult<MemoryRecord> {
1390 let kind_str: String = row.get(1)?;
1391 let kind = match kind_str.as_str() {
1392 "fact" => MemoryKind::Fact(Fact {
1393 subject: row.get(2)?,
1394 relation: row.get(3)?,
1395 object: row.get(4)?,
1396 }),
1397 _ => MemoryKind::Episode(Episode {
1398 text: row.get::<_, Option<String>>(5)?.unwrap_or_default(),
1399 }),
1400 };
1401 let embedding: Option<Vec<u8>> = row.get(7)?;
1402 let tags_str: String = row.get::<_, Option<String>>(11)?.unwrap_or_default();
1403 let tags: Vec<String> = if tags_str.is_empty() {
1404 vec![]
1405 } else {
1406 tags_str.split(',').map(|s| s.trim().to_string()).collect()
1407 };
1408 Ok(MemoryRecord {
1409 id: row.get(0)?,
1410 kind,
1411 strength: row.get(6)?,
1412 embedding: embedding.map(|b| blob_to_embedding(&b)),
1413 created_at: parse_datetime(&row.get::<_, String>(8)?),
1414 last_accessed_at: parse_datetime(&row.get::<_, String>(9)?),
1415 access_count: row.get(10)?,
1416 tags,
1417 source: row.get::<_, Option<String>>(12)?,
1418 session_id: row.get::<_, Option<String>>(13)?,
1419 channel: row.get::<_, Option<String>>(14)?,
1420 importance: row.get::<_, Option<f64>>(15)?.unwrap_or(0.5),
1421 namespace: row.get::<_, Option<String>>(16)?.unwrap_or_else(|| "default".to_string()),
1422 checksum: row.get::<_, Option<String>>(17)?,
1423 })
1424}