1use crate::db;
6use crate::db::{bytes_to_embedding, parse_optional_json, with_transaction};
7#[cfg(feature = "hnsw")]
8use crate::db::{enqueue_pending_index_op, PendingIndexOpKind};
9#[cfg(feature = "hnsw")]
10use crate::episodes;
11use crate::error::MemoryError;
12use crate::quantize::{self, Quantizer};
13use crate::types::{Fact, NamespaceDeleteReport};
14use crate::{merge_trace_ctx, MemoryStore};
15use rusqlite::{params, Connection};
16use stack_ids::TraceCtx;
17
18#[allow(dead_code)]
20pub fn insert_fact_with_fts(
21 conn: &Connection,
22 fact_id: &str,
23 namespace: &str,
24 content: &str,
25 embedding_bytes: &[u8],
26 source: Option<&str>,
27 metadata: Option<&serde_json::Value>,
28) -> Result<(), MemoryError> {
29 insert_fact_with_fts_q8(
30 conn,
31 fact_id,
32 namespace,
33 content,
34 embedding_bytes,
35 None,
36 source,
37 metadata,
38 None,
39 )
40}
41
42#[allow(clippy::too_many_arguments)]
44pub fn insert_fact_with_fts_q8(
45 conn: &Connection,
46 fact_id: &str,
47 namespace: &str,
48 content: &str,
49 embedding_bytes: &[u8],
50 q8_bytes: Option<&[u8]>,
51 source: Option<&str>,
52 metadata: Option<&serde_json::Value>,
53 sparse: Option<(&crate::SparseWeights, &str)>,
54) -> Result<(), MemoryError> {
55 let metadata_str = metadata.map(|m| m.to_string());
56 with_transaction(conn, |tx| {
57 tx.execute(
58 "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
59 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
60 params![
61 fact_id,
62 namespace,
63 content,
64 source,
65 embedding_bytes,
66 q8_bytes,
67 metadata_str
68 ],
69 )?;
70
71 tx.execute(
72 "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
73 params![fact_id],
74 )?;
75 let fts_rowid = tx.last_insert_rowid();
76
77 tx.execute(
78 "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
79 params![fts_rowid, content],
80 )?;
81
82 #[cfg(feature = "hnsw")]
83 enqueue_pending_index_op(
84 tx,
85 &format!("fact:{}", fact_id),
86 "fact",
87 PendingIndexOpKind::Upsert,
88 )?;
89 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
90 if let Some((weights, representation)) = sparse {
91 db::store_sparse_vector(tx, &format!("fact:{fact_id}"), weights, representation)?;
92 }
93
94 Ok(())
95 })
96}
97
98#[allow(clippy::too_many_arguments)]
102pub fn insert_fact_in_tx(
103 tx: &rusqlite::Transaction<'_>,
104 fact_id: &str,
105 namespace: &str,
106 content: &str,
107 embedding_bytes: &[u8],
108 q8_bytes: Option<&[u8]>,
109 source: Option<&str>,
110 metadata: Option<&serde_json::Value>,
111) -> Result<(), MemoryError> {
112 let metadata_str = metadata.map(|m| m.to_string());
113 tx.execute(
114 "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
115 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
116 params![
117 fact_id,
118 namespace,
119 content,
120 source,
121 embedding_bytes,
122 q8_bytes,
123 metadata_str
124 ],
125 )?;
126
127 tx.execute(
128 "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
129 params![fact_id],
130 )?;
131 let fts_rowid = tx.last_insert_rowid();
132
133 tx.execute(
134 "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
135 params![fts_rowid, content],
136 )?;
137
138 #[cfg(feature = "hnsw")]
139 enqueue_pending_index_op(
140 tx,
141 &format!("fact:{}", fact_id),
142 "fact",
143 PendingIndexOpKind::Upsert,
144 )?;
145 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
146
147 Ok(())
148}
149
150#[allow(dead_code)] pub fn delete_fact_with_fts(conn: &Connection, fact_id: &str) -> Result<(), MemoryError> {
153 with_transaction(conn, |tx| {
154 let fts_rowid: i64 = tx
155 .query_row(
156 "SELECT rowid FROM facts_rowid_map WHERE fact_id = ?1",
157 params![fact_id],
158 |row| row.get(0),
159 )
160 .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
161
162 let content: String = tx
163 .query_row(
164 "SELECT content FROM facts WHERE id = ?1",
165 params![fact_id],
166 |row| row.get(0),
167 )
168 .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
169
170 tx.execute(
171 "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
172 params![fts_rowid, content],
173 )?;
174 tx.execute(
175 "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
176 params![fact_id],
177 )?;
178 tx.execute(
179 "DELETE FROM episode_causes WHERE cause_node_id IN (?1, ?2)",
180 params![fact_id, format!("fact:{fact_id}")],
181 )?;
182 tx.execute(
183 "DELETE FROM derivation_edges
184 WHERE (source_kind = 'fact' AND source_id = ?1)
185 OR (target_kind = 'fact' AND target_id = ?1)",
186 params![fact_id],
187 )?;
188 tx.execute("DELETE FROM facts WHERE id = ?1", params![fact_id])?;
189
190 #[cfg(feature = "hnsw")]
191 enqueue_pending_index_op(
192 tx,
193 &format!("fact:{}", fact_id),
194 "fact",
195 PendingIndexOpKind::Delete,
196 )?;
197 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
198
199 Ok(())
200 })
201}
202
203#[allow(dead_code)] pub fn update_fact_with_fts(
206 conn: &Connection,
207 fact_id: &str,
208 new_content: &str,
209 new_embedding_bytes: &[u8],
210 new_q8_bytes: Option<&[u8]>,
211) -> Result<(), MemoryError> {
212 with_transaction(conn, |tx| {
213 let (fts_rowid, old_content): (i64, String) = tx
214 .query_row(
215 "SELECT fm.rowid, f.content
216 FROM facts f
217 JOIN facts_rowid_map fm ON fm.fact_id = f.id
218 WHERE f.id = ?1",
219 params![fact_id],
220 |row| Ok((row.get(0)?, row.get(1)?)),
221 )
222 .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
223
224 tx.execute(
225 "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
226 params![fts_rowid, old_content],
227 )?;
228
229 tx.execute(
230 "UPDATE facts
231 SET content = ?1,
232 embedding = ?2,
233 embedding_q8 = ?3,
234 updated_at = datetime('now')
235 WHERE id = ?4",
236 params![new_content, new_embedding_bytes, new_q8_bytes, fact_id],
237 )?;
238
239 tx.execute(
240 "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
241 params![fts_rowid, new_content],
242 )?;
243 tx.execute(
244 "DELETE FROM derivation_edges
245 WHERE (source_kind = 'fact' AND source_id = ?1)
246 OR (target_kind = 'fact' AND target_id = ?1)",
247 params![fact_id],
248 )?;
249
250 #[cfg(feature = "hnsw")]
251 enqueue_pending_index_op(
252 tx,
253 &format!("fact:{}", fact_id),
254 "fact",
255 PendingIndexOpKind::Upsert,
256 )?;
257 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
258
259 Ok(())
260 })
261}
262
263pub fn delete_namespace(
265 conn: &Connection,
266 namespace: &str,
267) -> Result<NamespaceDeleteReport, MemoryError> {
268 with_transaction(conn, |tx| {
269 let mut report = NamespaceDeleteReport::default();
270 let delete_session = |session_id: &str| -> Result<(usize, usize), MemoryError> {
271 let message_data: Vec<(i64, String, i64, bool)> = {
272 let mut stmt = tx.prepare(
273 "SELECT m.id, m.content, mm.rowid, m.embedding IS NOT NULL
274 FROM messages m
275 JOIN messages_rowid_map mm ON mm.message_id = m.id
276 WHERE m.session_id = ?1",
277 )?;
278 let rows = stmt.query_map(params![session_id], |row| {
279 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
280 })?;
281 rows.collect::<Result<Vec<_>, _>>()?
282 };
283
284 for (message_id, content, fts_rowid, has_embedding) in &message_data {
285 #[cfg(not(feature = "hnsw"))]
286 let _ = (message_id, has_embedding);
287 tx.execute(
288 "INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', ?1, ?2)",
289 params![fts_rowid, content],
290 )?;
291 #[cfg(feature = "hnsw")]
292 if *has_embedding {
293 enqueue_pending_index_op(
294 tx,
295 &format!("msg:{}", message_id),
296 "message",
297 PendingIndexOpKind::Delete,
298 )?;
299 }
300 }
301
302 let affected = tx.execute("DELETE FROM sessions WHERE id = ?1", params![session_id])?;
303 if affected == 0 {
304 return Err(MemoryError::SessionNotFound(session_id.to_string()));
305 }
306 let hnsw_ops = message_data
307 .iter()
308 .filter(|(_, _, _, has_embedding)| *has_embedding)
309 .count();
310 Ok((message_data.len(), hnsw_ops))
311 };
312
313 let document_ids: Vec<String> = {
314 let mut stmt = tx.prepare("SELECT id FROM documents WHERE namespace = ?1")?;
315 let ids = stmt
316 .query_map(params![namespace], |row| row.get(0))?
317 .collect::<Result<Vec<_>, _>>()?;
318 ids
319 };
320
321 let session_ids: Vec<String> = {
322 let mut stmt = tx.prepare("SELECT id, metadata FROM sessions")?;
323 let rows = stmt.query_map([], |row| {
324 Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
325 })?;
326 let mut ids = Vec::new();
327 for row in rows {
328 let (session_id, metadata_raw) = row?;
329 let metadata = parse_optional_json(
330 "sessions",
331 &session_id,
332 "metadata",
333 metadata_raw.as_deref(),
334 )?;
335 let namespace_matches = metadata
336 .as_ref()
337 .and_then(|value| {
338 value
339 .get("namespace")
340 .or_else(|| value.get("scope_namespace"))
341 })
342 .and_then(|value| value.as_str())
343 == Some(namespace);
344 if namespace_matches {
345 ids.push(session_id);
346 }
347 }
348 ids
349 };
350
351 for session_id in &session_ids {
352 let (messages, hnsw_ops) = delete_session(session_id)?;
353 report.messages += messages;
354 report.hnsw_ops += hnsw_ops;
355 }
356 report.sessions = session_ids.len();
357
358 let delete_derivation_edges_for_id = |kind: &str, id: &str| -> Result<(), MemoryError> {
359 tx.execute(
360 "DELETE FROM derivation_edges
361 WHERE (source_kind = ?1 AND source_id = ?2)
362 OR (target_kind = ?1 AND target_id = ?2)",
363 params![kind, id],
364 )?;
365 Ok(())
366 };
367
368 let delete_derivation_edges_for_ids =
369 |kind: &str, ids: &[String]| -> Result<(), MemoryError> {
370 for id in ids {
371 delete_derivation_edges_for_id(kind, id)?;
372 }
373 Ok(())
374 };
375
376 let facts: Vec<(String, i64, String)> = {
377 let mut stmt = tx.prepare(
378 "SELECT f.id, fm.rowid, f.content
379 FROM facts f
380 JOIN facts_rowid_map fm ON fm.fact_id = f.id
381 WHERE f.namespace = ?1",
382 )?;
383 let facts = stmt
384 .query_map(params![namespace], |row| {
385 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
386 })?
387 .collect::<Result<Vec<_>, _>>()?;
388 facts
389 };
390
391 for (fact_id, fts_rowid, content) in &facts {
392 tx.execute(
393 "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
394 params![fts_rowid, content],
395 )?;
396 tx.execute(
397 "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
398 params![fact_id],
399 )?;
400
401 #[cfg(feature = "hnsw")]
402 enqueue_pending_index_op(
403 tx,
404 &format!("fact:{}", fact_id),
405 "fact",
406 PendingIndexOpKind::Delete,
407 )?;
408 #[cfg(feature = "hnsw")]
409 {
410 report.hnsw_ops += 1;
411 }
412 }
413 tx.execute("DELETE FROM facts WHERE namespace = ?1", params![namespace])?;
414 report.facts = facts.len();
415
416 for doc_id in &document_ids {
417 let mut stmt = tx.prepare(
418 "SELECT c.id, c.content, cm.rowid
419 FROM chunks c
420 JOIN chunks_rowid_map cm ON cm.chunk_id = c.id
421 WHERE c.document_id = ?1",
422 )?;
423 let chunk_rows: Vec<(String, String, i64)> = stmt
424 .query_map(params![doc_id], |row| {
425 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
426 })?
427 .collect::<Result<Vec<_>, _>>()?;
428 report.chunks += chunk_rows.len();
429
430 for (chunk_id, content, fts_rowid) in &chunk_rows {
431 tx.execute(
432 "INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', ?1, ?2)",
433 params![fts_rowid, content],
434 )?;
435 tx.execute(
436 "DELETE FROM chunks_rowid_map WHERE chunk_id = ?1",
437 params![chunk_id],
438 )?;
439 #[cfg(feature = "hnsw")]
440 enqueue_pending_index_op(
441 tx,
442 &format!("chunk:{}", chunk_id),
443 "chunk",
444 PendingIndexOpKind::Delete,
445 )?;
446 #[cfg(feature = "hnsw")]
447 {
448 report.hnsw_ops += 1;
449 }
450 }
451
452 tx.execute("DELETE FROM chunks WHERE document_id = ?1", params![doc_id])?;
453 }
454
455 for doc_id in &document_ids {
456 let mut stmt = tx.prepare(
457 "SELECT e.episode_id, e.search_text, erm.rowid
458 FROM episodes e
459 JOIN episodes_rowid_map erm ON erm.episode_id = e.episode_id
460 WHERE e.document_id = ?1",
461 )?;
462 let episode_rows: Vec<(String, String, i64)> = stmt
463 .query_map(params![doc_id], |row| {
464 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
465 })?
466 .collect::<Result<Vec<_>, _>>()?;
467 report.episodes += episode_rows.len();
468
469 for (episode_id, search_text, fts_rowid) in &episode_rows {
470 tx.execute(
471 "INSERT INTO episodes_fts(episodes_fts, rowid, content) VALUES ('delete', ?1, ?2)",
472 params![fts_rowid, search_text],
473 )?;
474 tx.execute(
475 "DELETE FROM episodes_rowid_map WHERE episode_id = ?1",
476 params![episode_id],
477 )?;
478 tx.execute(
479 "DELETE FROM episode_causes WHERE episode_id = ?1",
480 params![episode_id],
481 )?;
482 #[cfg(feature = "hnsw")]
483 enqueue_pending_index_op(
484 tx,
485 &episodes::episode_item_key(episode_id),
486 "episode",
487 PendingIndexOpKind::Delete,
488 )?;
489 #[cfg(feature = "hnsw")]
490 {
491 report.hnsw_ops += 1;
492 }
493 }
494
495 tx.execute(
496 "DELETE FROM episodes WHERE document_id = ?1",
497 params![doc_id],
498 )?;
499 tx.execute("DELETE FROM documents WHERE id = ?1", params![doc_id])?;
500 }
501 report.documents = document_ids.len();
502
503 let claim_ids: Vec<String> = {
504 let mut stmt =
505 tx.prepare("SELECT claim_id FROM claim_versions WHERE scope_namespace = ?1")?;
506 let ids = stmt
507 .query_map(params![namespace], |row| row.get(0))?
508 .collect::<Result<Vec<_>, _>>()?;
509 ids
510 };
511
512 let claim_version_ids: Vec<String> = {
513 let mut stmt = tx.prepare(
514 "SELECT claim_version_id FROM claim_versions WHERE scope_namespace = ?1",
515 )?;
516 let ids = stmt
517 .query_map(params![namespace], |row| row.get(0))?
518 .collect::<Result<Vec<_>, _>>()?;
519 ids
520 };
521
522 let relation_version_ids: Vec<String> = {
523 let mut stmt = tx.prepare(
524 "SELECT relation_version_id FROM relation_versions WHERE scope_namespace = ?1",
525 )?;
526 let ids = stmt
527 .query_map(params![namespace], |row| row.get(0))?
528 .collect::<Result<Vec<_>, _>>()?;
529 ids
530 };
531
532 let alias_entity_ids: Vec<String> = {
533 let mut stmt = tx.prepare(
534 "SELECT canonical_entity_id FROM entity_aliases WHERE scope_namespace = ?1",
535 )?;
536 let ids = stmt
537 .query_map(params![namespace], |row| row.get(0))?
538 .collect::<Result<Vec<_>, _>>()?;
539 ids
540 };
541
542 let evidence_handles: Vec<String> = {
543 let mut stmt = tx.prepare(
544 "SELECT er.fetch_handle FROM evidence_refs er
545 JOIN projection_import_log pil ON er.source_envelope_id = pil.source_envelope_id
546 WHERE pil.scope_namespace = ?1",
547 )?;
548 let handles = stmt
549 .query_map(params![namespace], |row| row.get(0))?
550 .collect::<Result<Vec<_>, _>>()?;
551 handles
552 };
553
554 let episode_ids: Vec<String> = {
555 let mut stmt = tx.prepare(
556 "SELECT episode_id FROM episode_links
557 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
558 )?;
559 let ids = stmt
560 .query_map(params![namespace], |row| row.get(0))?
561 .collect::<Result<Vec<_>, _>>()?;
562 ids
563 };
564
565 delete_derivation_edges_for_ids("claim", &claim_ids)?;
566 delete_derivation_edges_for_ids("claim_version", &claim_version_ids)?;
567 delete_derivation_edges_for_ids("relation_version", &relation_version_ids)?;
568 delete_derivation_edges_for_ids("entity", &alias_entity_ids)?;
569 delete_derivation_edges_for_ids("evidence_ref", &evidence_handles)?;
570 delete_derivation_edges_for_ids("episode", &episode_ids)?;
571
572 report.projection_rows += tx.execute(
573 "DELETE FROM claim_versions WHERE scope_namespace = ?1",
574 params![namespace],
575 )?;
576 report.projection_rows += tx.execute(
577 "DELETE FROM relation_versions WHERE scope_namespace = ?1",
578 params![namespace],
579 )?;
580 report.projection_rows += tx.execute(
581 "DELETE FROM entity_aliases WHERE scope_namespace = ?1",
582 params![namespace],
583 )?;
584 report.projection_rows += tx.execute(
585 "DELETE FROM evidence_refs
586 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
587 params![namespace],
588 )?;
589 report.projection_rows += tx.execute(
590 "DELETE FROM episode_links
591 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
592 params![namespace],
593 )?;
594 report.projection_rows += tx.execute(
595 "DELETE FROM projection_import_failures WHERE scope_namespace = ?1",
596 params![namespace],
597 )?;
598 report.projection_rows += tx.execute(
599 "DELETE FROM projection_import_log WHERE scope_namespace = ?1",
600 params![namespace],
601 )?;
602
603 Ok(report)
604 })
605}
606
607pub fn get_fact(conn: &Connection, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
609 let result = conn.query_row(
610 "SELECT id, namespace, content, source, created_at, updated_at, metadata
611 FROM facts WHERE id = ?1",
612 params![fact_id],
613 |row| {
614 Ok((
615 row.get::<_, String>(0)?,
616 row.get::<_, String>(1)?,
617 row.get::<_, String>(2)?,
618 row.get::<_, Option<String>>(3)?,
619 row.get::<_, String>(4)?,
620 row.get::<_, String>(5)?,
621 row.get::<_, Option<String>>(6)?,
622 ))
623 },
624 );
625
626 match result {
627 Ok((id, namespace, content, source, created_at, updated_at, metadata_raw)) => {
628 Ok(Some(Fact {
629 metadata: parse_optional_json("facts", &id, "metadata", metadata_raw.as_deref())?,
630 id,
631 namespace,
632 content,
633 source,
634 created_at,
635 updated_at,
636 }))
637 }
638 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
639 Err(err) => Err(MemoryError::Database(err)),
640 }
641}
642
643pub fn get_fact_embedding(
645 conn: &Connection,
646 fact_id: &str,
647) -> Result<Option<Vec<f32>>, MemoryError> {
648 let result: Result<Option<Vec<u8>>, _> = conn.query_row(
649 "SELECT embedding FROM facts WHERE id = ?1",
650 params![fact_id],
651 |row| row.get(0),
652 );
653
654 match result {
655 Ok(Some(bytes)) => Ok(Some(bytes_to_embedding(&bytes)?)),
656 Ok(None) => Ok(None),
657 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
658 Err(err) => Err(MemoryError::Database(err)),
659 }
660}
661
662pub fn list_fact_namespaces(conn: &Connection) -> Result<Vec<String>, MemoryError> {
664 let mut stmt = conn.prepare("SELECT DISTINCT namespace FROM facts ORDER BY namespace")?;
665 let rows = stmt
666 .query_map([], |row| row.get::<_, String>(0))?
667 .collect::<Result<Vec<_>, _>>()?;
668 Ok(rows)
669}
670
671#[allow(dead_code)] pub fn list_facts(
674 conn: &Connection,
675 namespace: &str,
676 limit: usize,
677 offset: usize,
678) -> Result<Vec<Fact>, MemoryError> {
679 list_facts_with_view(conn, namespace, limit, offset, &StateView::Current)
680}
681
682#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
684pub enum StateView {
685 Current,
686 HistoricalAt(String),
687 RecordedAsOf(String),
688 IncludeSuperseded,
689}
690
691pub(crate) fn fact_is_visible_with_view(
692 conn: &Connection,
693 fact_id: &str,
694 view: &StateView,
695) -> Result<bool, MemoryError> {
696 let forgotten: bool = conn.query_row(
697 "SELECT EXISTS(SELECT 1 FROM forgotten_facts WHERE fact_id = ?1)",
698 params![fact_id],
699 |row| row.get(0),
700 )?;
701 if forgotten {
702 return Ok(false);
703 }
704 let cutoff = match view {
705 StateView::HistoricalAt(value) | StateView::RecordedAsOf(value) => {
706 let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|e| {
707 MemoryError::Other(format!("invalid StateView timestamp '{value}': {e}"))
708 })?;
709 Some(
710 parsed
711 .with_timezone(&chrono::Utc)
712 .format("%Y-%m-%d %H:%M:%S%.6f")
713 .to_string(),
714 )
715 }
716 _ => None,
717 };
718 let include_superseded = matches!(view, StateView::IncludeSuperseded);
719 let visible: i64 = conn.query_row(
720 "SELECT EXISTS(
721 SELECT 1 FROM facts f
722 WHERE f.id = ?1
723 AND (?2 IS NULL OR f.created_at <= ?2)
724 AND (?3 = 1 OR NOT EXISTS (
725 SELECT 1 FROM graph_edges ge
726 WHERE ge.target = 'fact:' || f.id
727 AND ge.is_invalidated = 0
728 AND COALESCE(
729 json_extract(ge.edge_type, '$.relation'),
730 json_extract(ge.edge_type, '$.entity.relation')
731 ) IN ('supersedes', 'redacts')
732 AND (?2 IS NULL OR COALESCE(ge.recorded_time, ge.recorded_at) <= ?2)
733 ))
734 )",
735 params![fact_id, cutoff, include_superseded],
736 |row| row.get(0),
737 )?;
738 Ok(visible != 0)
739}
740
741pub fn list_facts_with_view(
743 conn: &Connection,
744 namespace: &str,
745 limit: usize,
746 offset: usize,
747 view: &StateView,
748) -> Result<Vec<Fact>, MemoryError> {
749 let cutoff = match view {
750 StateView::HistoricalAt(value) | StateView::RecordedAsOf(value) => {
751 let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|e| {
752 MemoryError::Other(format!("invalid StateView timestamp '{value}': {e}"))
753 })?;
754 Some(
755 parsed
756 .with_timezone(&chrono::Utc)
757 .format("%Y-%m-%d %H:%M:%S%.6f")
758 .to_string(),
759 )
760 }
761 _ => None,
762 };
763 let inconsistent: i64 = conn.query_row(
764 "SELECT COUNT(*) FROM (
765 SELECT target FROM graph_edges
766 WHERE is_invalidated = 0
767 AND COALESCE(
768 json_extract(edge_type, '$.relation'),
769 json_extract(edge_type, '$.entity.relation')
770 ) IN ('supersedes', 'redacts')
771 AND (?1 IS NULL OR COALESCE(recorded_time, recorded_at) <= ?1)
772 GROUP BY target HAVING COUNT(DISTINCT source) > 1
773 )",
774 params![cutoff.as_deref()],
775 |row| row.get(0),
776 )?;
777 if inconsistent != 0 {
778 return Err(MemoryError::Other(
779 "inconsistent fact lineage: multiple active heads".into(),
780 ));
781 }
782 let include_superseded = matches!(view, StateView::IncludeSuperseded);
783 let mut stmt = conn.prepare(
784 "SELECT id, namespace, content, source, created_at, updated_at, metadata
785 FROM facts
786 WHERE namespace = ?1
787 AND NOT EXISTS (
788 SELECT 1 FROM forgotten_facts ff WHERE ff.fact_id = facts.id
789 )
790 AND (?4 IS NULL OR created_at <= ?4)
791 AND (?5 = 1 OR NOT EXISTS (
792 SELECT 1 FROM graph_edges ge
793 WHERE ge.target = 'fact:' || facts.id
794 AND ge.is_invalidated = 0
795 AND COALESCE(
796 json_extract(ge.edge_type, '$.relation'),
797 json_extract(ge.edge_type, '$.entity.relation')
798 ) IN ('supersedes', 'redacts')
799 AND (?4 IS NULL OR COALESCE(ge.recorded_time, ge.recorded_at) <= ?4)
800 ))
801 ORDER BY updated_at DESC
802 LIMIT ?2 OFFSET ?3",
803 )?;
804
805 let facts = stmt
806 .query_map(
807 params![
808 namespace,
809 limit as i64,
810 offset as i64,
811 cutoff,
812 include_superseded
813 ],
814 |row| {
815 Ok((
816 row.get::<_, String>(0)?,
817 row.get::<_, String>(1)?,
818 row.get::<_, String>(2)?,
819 row.get::<_, Option<String>>(3)?,
820 row.get::<_, String>(4)?,
821 row.get::<_, String>(5)?,
822 row.get::<_, Option<String>>(6)?,
823 ))
824 },
825 )?
826 .collect::<Result<Vec<_>, _>>()?
827 .into_iter()
828 .map(
829 |(id, namespace, content, source, created_at, updated_at, metadata_raw)| {
830 Ok(Fact {
831 metadata: parse_optional_json(
832 "facts",
833 &id,
834 "metadata",
835 metadata_raw.as_deref(),
836 )?,
837 id,
838 namespace,
839 content,
840 source,
841 created_at,
842 updated_at,
843 })
844 },
845 )
846 .collect::<Result<Vec<_>, MemoryError>>()?;
847
848 Ok(facts)
849}
850
851impl MemoryStore {
852 pub async fn add_fact_raw_compat(
857 &self,
858 namespace: &str,
859 content: &str,
860 source: Option<&str>,
861 metadata: Option<serde_json::Value>,
862 trace_ctx: Option<&TraceCtx>,
863 ) -> Result<Fact, MemoryError> {
864 let id = self
865 .add_fact_with_trace(namespace, content, source, metadata, trace_ctx)
866 .await?;
867 self.get_fact(&id)
868 .await?
869 .ok_or(MemoryError::FactNotFound(id))
870 }
871
872 pub async fn add_fact(
877 &self,
878 namespace: &str,
879 content: &str,
880 source: Option<&str>,
881 metadata: Option<serde_json::Value>,
882 ) -> Result<String, MemoryError> {
883 self.add_fact_with_trace(namespace, content, source, metadata, None)
884 .await
885 }
886
887 pub async fn add_fact_with_trace(
889 &self,
890 namespace: &str,
891 content: &str,
892 source: Option<&str>,
893 metadata: Option<serde_json::Value>,
894 trace_ctx: Option<&TraceCtx>,
895 ) -> Result<String, MemoryError> {
896 self.validate_content("fact.content", content)?;
897
898 let ns_check = namespace.to_string();
901 let ct_check = content.to_string();
902 let existing_id = self
903 .with_read_conn(move |conn| {
904 let result: Option<String> = conn
905 .query_row(
906 "SELECT id FROM facts WHERE content = ?1 AND namespace = ?2 LIMIT 1",
907 rusqlite::params![&ct_check, &ns_check],
908 |row| row.get::<_, String>(0),
909 )
910 .ok();
911 Ok(result)
912 })
913 .await?;
914
915 if let Some(id) = existing_id {
916 return Ok(id);
917 }
918
919 let (embedding, sparse, sparse_representation) =
920 self.embed_text_with_sparse_internal(content).await?;
921 self.validate_embedding_dimensions(&embedding)?;
922 let embedding_bytes = db::embedding_to_bytes(&embedding);
923 let fact_id = uuid::Uuid::new_v4().to_string();
924 let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
925
926 let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
927 let q8_bytes = quantizer
929 .quantize(&embedding)
930 .map(|qv| quantize::pack_quantized(&qv))
931 .ok();
932
933 let ns = namespace.to_string();
934 let ct = content.to_string();
935 let fid = fact_id.clone();
936 let src = source.map(|s| s.to_string());
937 let meta = merge_trace_ctx(metadata, trace_ctx);
938 self.with_write_conn(move |conn| {
939 let current_count: usize = conn.query_row(
940 "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
941 rusqlite::params![&ns],
942 |row| row.get(0),
943 )?;
944 if current_count >= max_facts_per_namespace {
945 return Err(MemoryError::NamespaceFull {
946 namespace: ns.clone(),
947 count: current_count,
948 limit: max_facts_per_namespace,
949 });
950 }
951 insert_fact_with_fts_q8(
952 conn,
953 &fid,
954 &ns,
955 &ct,
956 &embedding_bytes,
957 q8_bytes.as_deref(),
958 src.as_deref(),
959 meta.as_ref(),
960 sparse.as_ref().zip(sparse_representation.as_deref()),
961 )
962 })
963 .await?;
964
965 self.clear_search_cache();
966
967 #[cfg(feature = "hnsw")]
968 self.sync_pending_hnsw_ops_best_effort("add_fact").await;
969
970 Ok(fact_id)
971 }
972
973 pub async fn add_fact_with_embedding(
975 &self,
976 namespace: &str,
977 content: &str,
978 embedding: &[f32],
979 source: Option<&str>,
980 metadata: Option<serde_json::Value>,
981 ) -> Result<String, MemoryError> {
982 self.add_fact_with_embedding_and_trace(
983 namespace, content, embedding, source, metadata, None,
984 )
985 .await
986 }
987
988 pub async fn add_fact_with_embedding_and_trace(
990 &self,
991 namespace: &str,
992 content: &str,
993 embedding: &[f32],
994 source: Option<&str>,
995 metadata: Option<serde_json::Value>,
996 trace_ctx: Option<&TraceCtx>,
997 ) -> Result<String, MemoryError> {
998 self.validate_content("fact.content", content)?;
999 self.validate_embedding_dimensions(embedding)?;
1000 let embedding_bytes = db::embedding_to_bytes(embedding);
1001 let sparse = self.inner.config.search.derive_sparse_from_dense.then(|| {
1002 crate::SparseWeights::from_dense(
1003 embedding,
1004 self.inner.config.search.sparse_derive_top_k,
1005 self.inner.config.search.sparse_derive_min_weight,
1006 )
1007 });
1008 let fact_id = uuid::Uuid::new_v4().to_string();
1009 let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
1010
1011 let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
1012 let q8_bytes = quantizer
1014 .quantize(embedding)
1015 .map(|qv| quantize::pack_quantized(&qv))
1016 .ok();
1017
1018 let ns = namespace.to_string();
1019 let ct = content.to_string();
1020 let fid = fact_id.clone();
1021 let src = source.map(|s| s.to_string());
1022 let meta = merge_trace_ctx(metadata, trace_ctx);
1023 self.with_write_conn(move |conn| {
1024 let current_count: usize = conn.query_row(
1025 "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
1026 rusqlite::params![&ns],
1027 |row| row.get(0),
1028 )?;
1029 if current_count >= max_facts_per_namespace {
1030 return Err(MemoryError::NamespaceFull {
1031 namespace: ns.clone(),
1032 count: current_count,
1033 limit: max_facts_per_namespace,
1034 });
1035 }
1036 insert_fact_with_fts_q8(
1037 conn,
1038 &fid,
1039 &ns,
1040 &ct,
1041 &embedding_bytes,
1042 q8_bytes.as_deref(),
1043 src.as_deref(),
1044 meta.as_ref(),
1045 sparse
1046 .as_ref()
1047 .map(|weights| (weights, "generic_dense_derived_sparse")),
1048 )
1049 })
1050 .await?;
1051
1052 self.clear_search_cache();
1053
1054 #[cfg(feature = "hnsw")]
1055 self.sync_pending_hnsw_ops_best_effort("add_fact_with_embedding")
1056 .await;
1057
1058 Ok(fact_id)
1059 }
1060
1061 #[cfg(feature = "admin-ops")]
1066 pub async fn update_fact(&self, fact_id: &str, content: &str) -> Result<(), MemoryError> {
1067 self.validate_content("fact.content", content)?;
1068 let (embedding, sparse, sparse_representation) =
1069 self.embed_text_with_sparse_internal(content).await?;
1070 self.validate_embedding_dimensions(&embedding)?;
1071 let embedding_bytes = db::embedding_to_bytes(&embedding);
1072 let q8_bytes = Quantizer::new(self.inner.config.embedding.dimensions)
1074 .quantize(&embedding)
1075 .map(|qv| quantize::pack_quantized(&qv))
1076 .ok();
1077
1078 let fid = fact_id.to_string();
1079 let ct = content.to_string();
1080 self.with_write_conn(move |conn| {
1081 update_fact_with_fts(conn, &fid, &ct, &embedding_bytes, q8_bytes.as_deref())?;
1082 let item_key = format!("fact:{fid}");
1083 if let Some((weights, representation)) =
1084 sparse.as_ref().zip(sparse_representation.as_deref())
1085 {
1086 db::store_sparse_vector(conn, &item_key, weights, representation)?;
1087 } else {
1088 db::delete_sparse_vector(conn, &item_key)?;
1089 }
1090 Ok(())
1091 })
1092 .await?;
1093
1094 #[cfg(feature = "hnsw")]
1095 self.sync_pending_hnsw_ops_best_effort("update_fact").await;
1096
1097 self.clear_search_cache();
1098
1099 Ok(())
1100 }
1101
1102 #[cfg(feature = "admin-ops")]
1107 pub async fn delete_fact(&self, fact_id: &str) -> Result<(), MemoryError> {
1108 let fid = fact_id.to_string();
1109 self.with_write_conn(move |conn| delete_fact_with_fts(conn, &fid))
1110 .await?;
1111
1112 #[cfg(feature = "hnsw")]
1113 self.sync_pending_hnsw_ops_best_effort("delete_fact").await;
1114
1115 self.clear_search_cache();
1116
1117 Ok(())
1118 }
1119
1120 pub async fn delete_namespace(
1122 &self,
1123 namespace: &str,
1124 ) -> Result<NamespaceDeleteReport, MemoryError> {
1125 let ns = namespace.to_string();
1126 let count = self
1127 .with_write_conn(move |conn| delete_namespace(conn, &ns))
1128 .await?;
1129
1130 #[cfg(feature = "hnsw")]
1131 self.sync_pending_hnsw_ops_best_effort("delete_namespace")
1132 .await;
1133
1134 self.clear_search_cache();
1135
1136 Ok(count)
1137 }
1138
1139 pub async fn get_fact(&self, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
1141 let fid = fact_id.to_string();
1142 self.with_read_conn(move |conn| get_fact(conn, &fid)).await
1143 }
1144
1145 pub async fn get_fact_raw_compat(&self, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
1147 self.get_fact(fact_id).await
1148 }
1149
1150 pub async fn get_fact_embedding(&self, fact_id: &str) -> Result<Option<Vec<f32>>, MemoryError> {
1152 let fid = fact_id.to_string();
1153 self.with_read_conn(move |conn| get_fact_embedding(conn, &fid))
1154 .await
1155 }
1156
1157 pub async fn list_facts(
1159 &self,
1160 namespace: &str,
1161 limit: usize,
1162 offset: usize,
1163 ) -> Result<Vec<Fact>, MemoryError> {
1164 self.list_facts_with_view(namespace, limit, offset, StateView::Current)
1165 .await
1166 }
1167
1168 pub async fn list_facts_with_view(
1170 &self,
1171 namespace: &str,
1172 limit: usize,
1173 offset: usize,
1174 view: StateView,
1175 ) -> Result<Vec<Fact>, MemoryError> {
1176 let ns = namespace.to_string();
1177 self.with_read_conn(move |conn| list_facts_with_view(conn, &ns, limit, offset, &view))
1178 .await
1179 }
1180
1181 pub async fn list_fact_namespaces(&self) -> Result<Vec<String>, MemoryError> {
1183 self.with_read_conn(move |conn| list_fact_namespaces(conn))
1184 .await
1185 }
1186}
1187
1188#[cfg(test)]
1189mod state_view_regression_tests {
1190 use super::*;
1191 use crate::db::run_migrations;
1192 use rusqlite::Connection;
1193
1194 fn seeded() -> Connection {
1195 let conn = Connection::open_in_memory().unwrap();
1196 run_migrations(&conn).unwrap();
1197 for (id, content, created) in [
1198 ("old", "same topic old", "2026-07-10 21:00:00"),
1199 ("new", "same topic new", "2026-07-10 21:12:01"),
1200 ] {
1201 conn.execute(
1202 "INSERT INTO facts(id, namespace, content, created_at, updated_at) VALUES (?1, 'n', ?2, ?3, ?3)",
1203 params![id, content, created],
1204 ).unwrap();
1205 }
1206 conn
1207 }
1208
1209 fn supersedes(conn: &Connection, source: &str, target: &str, recorded: &str) {
1210 conn.execute(
1211 "INSERT INTO graph_edges(id, source, target, edge_type, weight, content_digest, recorded_at, valid_time, recorded_time)
1212 VALUES (lower(hex(randomblob(16))), ?1, ?2, '{\"type\":\"entity\",\"relation\":\"supersedes\"}', 1, lower(hex(randomblob(16))), ?3, ?3, ?3)",
1213 params![format!("fact:{source}"), format!("fact:{target}"), recorded],
1214 ).unwrap();
1215 }
1216
1217 fn supersedes_canonical(conn: &Connection, source: &str, target: &str, recorded: &str) {
1218 conn.execute(
1219 "INSERT INTO graph_edges(id, source, target, edge_type, weight, content_digest, recorded_at, valid_time, recorded_time)
1220 VALUES (lower(hex(randomblob(16))), ?1, ?2, '{\"entity\":{\"relation\":\"supersedes\"}}', 1, lower(hex(randomblob(16))), ?3, ?3, ?3)",
1221 params![format!("fact:{source}"), format!("fact:{target}"), recorded],
1222 ).unwrap();
1223 }
1224
1225 #[test]
1226 fn historical_view_excludes_future_fact_and_reconstructs_pre_supersession_head() {
1227 let conn = seeded();
1228 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1229 let rows = list_facts_with_view(
1230 &conn,
1231 "n",
1232 10,
1233 0,
1234 &StateView::HistoricalAt("2026-07-10T21:11:50Z".into()),
1235 )
1236 .unwrap();
1237 assert_eq!(
1238 rows.iter().map(|f| f.id.as_str()).collect::<Vec<_>>(),
1239 ["old"]
1240 );
1241 }
1242
1243 #[test]
1244 fn historical_view_preserves_pre_adjudication_conflict() {
1245 let conn = seeded();
1246 conn.execute(
1247 "UPDATE facts SET created_at = '2026-07-10 21:10:00', updated_at = '2026-07-10 21:10:00' WHERE id = 'new'",
1248 [],
1249 )
1250 .unwrap();
1251 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1252
1253 let rows = list_facts_with_view(
1254 &conn,
1255 "n",
1256 10,
1257 0,
1258 &StateView::HistoricalAt("2026-07-10T21:11:50Z".into()),
1259 )
1260 .unwrap();
1261 let ids = rows.iter().map(|fact| fact.id.as_str()).collect::<Vec<_>>();
1262 assert!(
1263 ids.contains(&"old"),
1264 "prior observation must remain visible"
1265 );
1266 assert!(ids.contains(&"new"), "conflicting observation created before the cutoff must remain visible until adjudication");
1267 }
1268
1269 #[test]
1270 fn current_view_excludes_superseded_fact() {
1271 let conn = seeded();
1272 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1273 let rows = list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).unwrap();
1274 assert_eq!(
1275 rows.iter().map(|f| f.id.as_str()).collect::<Vec<_>>(),
1276 ["new"]
1277 );
1278 }
1279
1280 #[test]
1281 fn current_view_accepts_canonical_entity_edge_serialization() {
1282 let conn = seeded();
1283 supersedes_canonical(&conn, "new", "old", "2026-07-10 21:12:01");
1284 let rows = list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).unwrap();
1285 assert_eq!(
1286 rows.iter().map(|fact| fact.id.as_str()).collect::<Vec<_>>(),
1287 vec!["new"]
1288 );
1289 }
1290
1291 #[test]
1292 fn multiple_active_heads_fail_closed() {
1293 let conn = seeded();
1294 conn.execute("INSERT INTO facts(id, namespace, content, created_at, updated_at) VALUES ('other', 'n', 'same topic conflicting', '2026-07-10 21:13:00', '2026-07-10 21:13:00')", []).unwrap();
1295 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1296 supersedes(&conn, "other", "old", "2026-07-10 21:13:00");
1297 assert!(list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).is_err());
1298 }
1299}