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