Skip to main content

semantic_memory/
knowledge.rs

1//! Fact CRUD with FTS5 synchronization.
2//!
3//! Every fact operation that touches `facts_fts` is transactional.
4
5use 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/// Insert a fact and its FTS entry in a transaction.
19#[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    )
39}
40
41/// Insert a fact with both f32 and quantized embeddings.
42#[allow(clippy::too_many_arguments)]
43pub fn insert_fact_with_fts_q8(
44    conn: &Connection,
45    fact_id: &str,
46    namespace: &str,
47    content: &str,
48    embedding_bytes: &[u8],
49    q8_bytes: Option<&[u8]>,
50    source: Option<&str>,
51    metadata: Option<&serde_json::Value>,
52) -> Result<(), MemoryError> {
53    let metadata_str = metadata.map(|m| m.to_string());
54    with_transaction(conn, |tx| {
55        tx.execute(
56            "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
57             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
58            params![
59                fact_id,
60                namespace,
61                content,
62                source,
63                embedding_bytes,
64                q8_bytes,
65                metadata_str
66            ],
67        )?;
68
69        tx.execute(
70            "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
71            params![fact_id],
72        )?;
73        let fts_rowid = tx.last_insert_rowid();
74
75        tx.execute(
76            "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
77            params![fts_rowid, content],
78        )?;
79
80        #[cfg(feature = "hnsw")]
81        enqueue_pending_index_op(
82            tx,
83            &format!("fact:{}", fact_id),
84            "fact",
85            PendingIndexOpKind::Upsert,
86        )?;
87        db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
88
89        Ok(())
90    })
91}
92
93/// Insert a fact within an existing transaction (no nested transaction).
94///
95/// Used by the import boundary where the outer transaction is already active.
96#[allow(clippy::too_many_arguments)]
97pub fn insert_fact_in_tx(
98    tx: &rusqlite::Transaction<'_>,
99    fact_id: &str,
100    namespace: &str,
101    content: &str,
102    embedding_bytes: &[u8],
103    q8_bytes: Option<&[u8]>,
104    source: Option<&str>,
105    metadata: Option<&serde_json::Value>,
106) -> Result<(), MemoryError> {
107    let metadata_str = metadata.map(|m| m.to_string());
108    tx.execute(
109        "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
110         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
111        params![
112            fact_id,
113            namespace,
114            content,
115            source,
116            embedding_bytes,
117            q8_bytes,
118            metadata_str
119        ],
120    )?;
121
122    tx.execute(
123        "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
124        params![fact_id],
125    )?;
126    let fts_rowid = tx.last_insert_rowid();
127
128    tx.execute(
129        "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
130        params![fts_rowid, content],
131    )?;
132
133    #[cfg(feature = "hnsw")]
134    enqueue_pending_index_op(
135        tx,
136        &format!("fact:{}", fact_id),
137        "fact",
138        PendingIndexOpKind::Upsert,
139    )?;
140    db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
141
142    Ok(())
143}
144
145/// Delete a fact and its FTS entry in a transaction.
146#[allow(dead_code)] // public API — used by external consumers, not internally
147pub fn delete_fact_with_fts(conn: &Connection, fact_id: &str) -> Result<(), MemoryError> {
148    with_transaction(conn, |tx| {
149        let fts_rowid: i64 = tx
150            .query_row(
151                "SELECT rowid FROM facts_rowid_map WHERE fact_id = ?1",
152                params![fact_id],
153                |row| row.get(0),
154            )
155            .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
156
157        let content: String = tx
158            .query_row(
159                "SELECT content FROM facts WHERE id = ?1",
160                params![fact_id],
161                |row| row.get(0),
162            )
163            .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
164
165        tx.execute(
166            "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
167            params![fts_rowid, content],
168        )?;
169        tx.execute(
170            "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
171            params![fact_id],
172        )?;
173        tx.execute(
174            "DELETE FROM episode_causes WHERE cause_node_id IN (?1, ?2)",
175            params![fact_id, format!("fact:{fact_id}")],
176        )?;
177        tx.execute(
178            "DELETE FROM derivation_edges
179             WHERE (source_kind = 'fact' AND source_id = ?1)
180                OR (target_kind = 'fact' AND target_id = ?1)",
181            params![fact_id],
182        )?;
183        tx.execute("DELETE FROM facts WHERE id = ?1", params![fact_id])?;
184
185        #[cfg(feature = "hnsw")]
186        enqueue_pending_index_op(
187            tx,
188            &format!("fact:{}", fact_id),
189            "fact",
190            PendingIndexOpKind::Delete,
191        )?;
192        db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
193
194        Ok(())
195    })
196}
197
198/// Update a fact's content and embeddings, with FTS synchronization.
199#[allow(dead_code)] // public API — used by external consumers, not internally
200pub fn update_fact_with_fts(
201    conn: &Connection,
202    fact_id: &str,
203    new_content: &str,
204    new_embedding_bytes: &[u8],
205    new_q8_bytes: Option<&[u8]>,
206) -> Result<(), MemoryError> {
207    with_transaction(conn, |tx| {
208        let (fts_rowid, old_content): (i64, String) = tx
209            .query_row(
210                "SELECT fm.rowid, f.content
211                 FROM facts f
212                 JOIN facts_rowid_map fm ON fm.fact_id = f.id
213                 WHERE f.id = ?1",
214                params![fact_id],
215                |row| Ok((row.get(0)?, row.get(1)?)),
216            )
217            .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
218
219        tx.execute(
220            "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
221            params![fts_rowid, old_content],
222        )?;
223
224        tx.execute(
225            "UPDATE facts
226             SET content = ?1,
227                 embedding = ?2,
228                 embedding_q8 = ?3,
229                 updated_at = datetime('now')
230             WHERE id = ?4",
231            params![new_content, new_embedding_bytes, new_q8_bytes, fact_id],
232        )?;
233
234        tx.execute(
235            "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
236            params![fts_rowid, new_content],
237        )?;
238        tx.execute(
239            "DELETE FROM derivation_edges
240             WHERE (source_kind = 'fact' AND source_id = ?1)
241                OR (target_kind = 'fact' AND target_id = ?1)",
242            params![fact_id],
243        )?;
244
245        #[cfg(feature = "hnsw")]
246        enqueue_pending_index_op(
247            tx,
248            &format!("fact:{}", fact_id),
249            "fact",
250            PendingIndexOpKind::Upsert,
251        )?;
252        db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
253
254        Ok(())
255    })
256}
257
258/// Delete all namespace-scoped memory atomically and report every affected surface.
259pub fn delete_namespace(
260    conn: &Connection,
261    namespace: &str,
262) -> Result<NamespaceDeleteReport, MemoryError> {
263    with_transaction(conn, |tx| {
264        let mut report = NamespaceDeleteReport::default();
265        let delete_session = |session_id: &str| -> Result<(usize, usize), MemoryError> {
266            let message_data: Vec<(i64, String, i64, bool)> = {
267                let mut stmt = tx.prepare(
268                    "SELECT m.id, m.content, mm.rowid, m.embedding IS NOT NULL
269                     FROM messages m
270                     JOIN messages_rowid_map mm ON mm.message_id = m.id
271                     WHERE m.session_id = ?1",
272                )?;
273                let rows = stmt.query_map(params![session_id], |row| {
274                    Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
275                })?;
276                rows.collect::<Result<Vec<_>, _>>()?
277            };
278
279            for (message_id, content, fts_rowid, has_embedding) in &message_data {
280                #[cfg(not(feature = "hnsw"))]
281                let _ = (message_id, has_embedding);
282                tx.execute(
283                    "INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', ?1, ?2)",
284                    params![fts_rowid, content],
285                )?;
286                #[cfg(feature = "hnsw")]
287                if *has_embedding {
288                    enqueue_pending_index_op(
289                        tx,
290                        &format!("msg:{}", message_id),
291                        "message",
292                        PendingIndexOpKind::Delete,
293                    )?;
294                }
295            }
296
297            let affected = tx.execute("DELETE FROM sessions WHERE id = ?1", params![session_id])?;
298            if affected == 0 {
299                return Err(MemoryError::SessionNotFound(session_id.to_string()));
300            }
301            let hnsw_ops = message_data
302                .iter()
303                .filter(|(_, _, _, has_embedding)| *has_embedding)
304                .count();
305            Ok((message_data.len(), hnsw_ops))
306        };
307
308        let document_ids: Vec<String> = {
309            let mut stmt = tx.prepare("SELECT id FROM documents WHERE namespace = ?1")?;
310            let ids = stmt
311                .query_map(params![namespace], |row| row.get(0))?
312                .collect::<Result<Vec<_>, _>>()?;
313            ids
314        };
315
316        let session_ids: Vec<String> = {
317            let mut stmt = tx.prepare("SELECT id, metadata FROM sessions")?;
318            let rows = stmt.query_map([], |row| {
319                Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
320            })?;
321            let mut ids = Vec::new();
322            for row in rows {
323                let (session_id, metadata_raw) = row?;
324                let metadata = parse_optional_json(
325                    "sessions",
326                    &session_id,
327                    "metadata",
328                    metadata_raw.as_deref(),
329                )?;
330                let namespace_matches = metadata
331                    .as_ref()
332                    .and_then(|value| {
333                        value
334                            .get("namespace")
335                            .or_else(|| value.get("scope_namespace"))
336                    })
337                    .and_then(|value| value.as_str())
338                    == Some(namespace);
339                if namespace_matches {
340                    ids.push(session_id);
341                }
342            }
343            ids
344        };
345
346        for session_id in &session_ids {
347            let (messages, hnsw_ops) = delete_session(session_id)?;
348            report.messages += messages;
349            report.hnsw_ops += hnsw_ops;
350        }
351        report.sessions = session_ids.len();
352
353        let delete_derivation_edges_for_id = |kind: &str, id: &str| -> Result<(), MemoryError> {
354            tx.execute(
355                "DELETE FROM derivation_edges
356                 WHERE (source_kind = ?1 AND source_id = ?2)
357                    OR (target_kind = ?1 AND target_id = ?2)",
358                params![kind, id],
359            )?;
360            Ok(())
361        };
362
363        let delete_derivation_edges_for_ids =
364            |kind: &str, ids: &[String]| -> Result<(), MemoryError> {
365                for id in ids {
366                    delete_derivation_edges_for_id(kind, id)?;
367                }
368                Ok(())
369            };
370
371        let facts: Vec<(String, i64, String)> = {
372            let mut stmt = tx.prepare(
373                "SELECT f.id, fm.rowid, f.content
374                 FROM facts f
375                 JOIN facts_rowid_map fm ON fm.fact_id = f.id
376                 WHERE f.namespace = ?1",
377            )?;
378            let facts = stmt
379                .query_map(params![namespace], |row| {
380                    Ok((row.get(0)?, row.get(1)?, row.get(2)?))
381                })?
382                .collect::<Result<Vec<_>, _>>()?;
383            facts
384        };
385
386        for (fact_id, fts_rowid, content) in &facts {
387            tx.execute(
388                "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
389                params![fts_rowid, content],
390            )?;
391            tx.execute(
392                "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
393                params![fact_id],
394            )?;
395
396            #[cfg(feature = "hnsw")]
397            enqueue_pending_index_op(
398                tx,
399                &format!("fact:{}", fact_id),
400                "fact",
401                PendingIndexOpKind::Delete,
402            )?;
403            #[cfg(feature = "hnsw")]
404            {
405                report.hnsw_ops += 1;
406            }
407        }
408        tx.execute("DELETE FROM facts WHERE namespace = ?1", params![namespace])?;
409        report.facts = facts.len();
410
411        for doc_id in &document_ids {
412            let mut stmt = tx.prepare(
413                "SELECT c.id, c.content, cm.rowid
414                 FROM chunks c
415                 JOIN chunks_rowid_map cm ON cm.chunk_id = c.id
416                 WHERE c.document_id = ?1",
417            )?;
418            let chunk_rows: Vec<(String, String, i64)> = stmt
419                .query_map(params![doc_id], |row| {
420                    Ok((row.get(0)?, row.get(1)?, row.get(2)?))
421                })?
422                .collect::<Result<Vec<_>, _>>()?;
423            report.chunks += chunk_rows.len();
424
425            for (chunk_id, content, fts_rowid) in &chunk_rows {
426                tx.execute(
427                    "INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', ?1, ?2)",
428                    params![fts_rowid, content],
429                )?;
430                tx.execute(
431                    "DELETE FROM chunks_rowid_map WHERE chunk_id = ?1",
432                    params![chunk_id],
433                )?;
434                #[cfg(feature = "hnsw")]
435                enqueue_pending_index_op(
436                    tx,
437                    &format!("chunk:{}", chunk_id),
438                    "chunk",
439                    PendingIndexOpKind::Delete,
440                )?;
441                #[cfg(feature = "hnsw")]
442                {
443                    report.hnsw_ops += 1;
444                }
445            }
446
447            tx.execute("DELETE FROM chunks WHERE document_id = ?1", params![doc_id])?;
448        }
449
450        for doc_id in &document_ids {
451            let mut stmt = tx.prepare(
452                "SELECT e.episode_id, e.search_text, erm.rowid
453                 FROM episodes e
454                 JOIN episodes_rowid_map erm ON erm.episode_id = e.episode_id
455                 WHERE e.document_id = ?1",
456            )?;
457            let episode_rows: Vec<(String, String, i64)> = stmt
458                .query_map(params![doc_id], |row| {
459                    Ok((row.get(0)?, row.get(1)?, row.get(2)?))
460                })?
461                .collect::<Result<Vec<_>, _>>()?;
462            report.episodes += episode_rows.len();
463
464            for (episode_id, search_text, fts_rowid) in &episode_rows {
465                tx.execute(
466                    "INSERT INTO episodes_fts(episodes_fts, rowid, content) VALUES ('delete', ?1, ?2)",
467                    params![fts_rowid, search_text],
468                )?;
469                tx.execute(
470                    "DELETE FROM episodes_rowid_map WHERE episode_id = ?1",
471                    params![episode_id],
472                )?;
473                tx.execute(
474                    "DELETE FROM episode_causes WHERE episode_id = ?1",
475                    params![episode_id],
476                )?;
477                #[cfg(feature = "hnsw")]
478                enqueue_pending_index_op(
479                    tx,
480                    &episodes::episode_item_key(episode_id),
481                    "episode",
482                    PendingIndexOpKind::Delete,
483                )?;
484                #[cfg(feature = "hnsw")]
485                {
486                    report.hnsw_ops += 1;
487                }
488            }
489
490            tx.execute(
491                "DELETE FROM episodes WHERE document_id = ?1",
492                params![doc_id],
493            )?;
494            tx.execute("DELETE FROM documents WHERE id = ?1", params![doc_id])?;
495        }
496        report.documents = document_ids.len();
497
498        let claim_ids: Vec<String> = {
499            let mut stmt =
500                tx.prepare("SELECT claim_id FROM claim_versions WHERE scope_namespace = ?1")?;
501            let ids = stmt
502                .query_map(params![namespace], |row| row.get(0))?
503                .collect::<Result<Vec<_>, _>>()?;
504            ids
505        };
506
507        let claim_version_ids: Vec<String> = {
508            let mut stmt = tx.prepare(
509                "SELECT claim_version_id FROM claim_versions WHERE scope_namespace = ?1",
510            )?;
511            let ids = stmt
512                .query_map(params![namespace], |row| row.get(0))?
513                .collect::<Result<Vec<_>, _>>()?;
514            ids
515        };
516
517        let relation_version_ids: Vec<String> = {
518            let mut stmt = tx.prepare(
519                "SELECT relation_version_id FROM relation_versions WHERE scope_namespace = ?1",
520            )?;
521            let ids = stmt
522                .query_map(params![namespace], |row| row.get(0))?
523                .collect::<Result<Vec<_>, _>>()?;
524            ids
525        };
526
527        let alias_entity_ids: Vec<String> = {
528            let mut stmt = tx.prepare(
529                "SELECT canonical_entity_id FROM entity_aliases WHERE scope_namespace = ?1",
530            )?;
531            let ids = stmt
532                .query_map(params![namespace], |row| row.get(0))?
533                .collect::<Result<Vec<_>, _>>()?;
534            ids
535        };
536
537        let evidence_handles: Vec<String> = {
538            let mut stmt = tx.prepare(
539                "SELECT er.fetch_handle FROM evidence_refs er
540                 JOIN projection_import_log pil ON er.source_envelope_id = pil.source_envelope_id
541                 WHERE pil.scope_namespace = ?1",
542            )?;
543            let handles = stmt
544                .query_map(params![namespace], |row| row.get(0))?
545                .collect::<Result<Vec<_>, _>>()?;
546            handles
547        };
548
549        let episode_ids: Vec<String> = {
550            let mut stmt = tx.prepare(
551                "SELECT episode_id FROM episode_links
552                 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
553            )?;
554            let ids = stmt
555                .query_map(params![namespace], |row| row.get(0))?
556                .collect::<Result<Vec<_>, _>>()?;
557            ids
558        };
559
560        delete_derivation_edges_for_ids("claim", &claim_ids)?;
561        delete_derivation_edges_for_ids("claim_version", &claim_version_ids)?;
562        delete_derivation_edges_for_ids("relation_version", &relation_version_ids)?;
563        delete_derivation_edges_for_ids("entity", &alias_entity_ids)?;
564        delete_derivation_edges_for_ids("evidence_ref", &evidence_handles)?;
565        delete_derivation_edges_for_ids("episode", &episode_ids)?;
566
567        report.projection_rows += tx.execute(
568            "DELETE FROM claim_versions WHERE scope_namespace = ?1",
569            params![namespace],
570        )?;
571        report.projection_rows += tx.execute(
572            "DELETE FROM relation_versions WHERE scope_namespace = ?1",
573            params![namespace],
574        )?;
575        report.projection_rows += tx.execute(
576            "DELETE FROM entity_aliases WHERE scope_namespace = ?1",
577            params![namespace],
578        )?;
579        report.projection_rows += tx.execute(
580            "DELETE FROM evidence_refs
581             WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
582            params![namespace],
583        )?;
584        report.projection_rows += tx.execute(
585            "DELETE FROM episode_links
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 projection_import_failures WHERE scope_namespace = ?1",
591            params![namespace],
592        )?;
593        report.projection_rows += tx.execute(
594            "DELETE FROM projection_import_log WHERE scope_namespace = ?1",
595            params![namespace],
596        )?;
597
598        Ok(report)
599    })
600}
601
602/// Get a fact by ID.
603pub fn get_fact(conn: &Connection, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
604    let result = conn.query_row(
605        "SELECT id, namespace, content, source, created_at, updated_at, metadata
606         FROM facts WHERE id = ?1",
607        params![fact_id],
608        |row| {
609            Ok((
610                row.get::<_, String>(0)?,
611                row.get::<_, String>(1)?,
612                row.get::<_, String>(2)?,
613                row.get::<_, Option<String>>(3)?,
614                row.get::<_, String>(4)?,
615                row.get::<_, String>(5)?,
616                row.get::<_, Option<String>>(6)?,
617            ))
618        },
619    );
620
621    match result {
622        Ok((id, namespace, content, source, created_at, updated_at, metadata_raw)) => {
623            Ok(Some(Fact {
624                metadata: parse_optional_json("facts", &id, "metadata", metadata_raw.as_deref())?,
625                id,
626                namespace,
627                content,
628                source,
629                created_at,
630                updated_at,
631            }))
632        }
633        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
634        Err(err) => Err(MemoryError::Database(err)),
635    }
636}
637
638/// Get a fact embedding vector.
639pub fn get_fact_embedding(
640    conn: &Connection,
641    fact_id: &str,
642) -> Result<Option<Vec<f32>>, MemoryError> {
643    let result: Result<Option<Vec<u8>>, _> = conn.query_row(
644        "SELECT embedding FROM facts WHERE id = ?1",
645        params![fact_id],
646        |row| row.get(0),
647    );
648
649    match result {
650        Ok(Some(bytes)) => Ok(Some(bytes_to_embedding(&bytes)?)),
651        Ok(None) => Ok(None),
652        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
653        Err(err) => Err(MemoryError::Database(err)),
654    }
655}
656
657/// List the distinct namespaces that currently contain facts.
658pub fn list_fact_namespaces(conn: &Connection) -> Result<Vec<String>, MemoryError> {
659    let mut stmt = conn.prepare("SELECT DISTINCT namespace FROM facts ORDER BY namespace")?;
660    let rows = stmt
661        .query_map([], |row| row.get::<_, String>(0))?
662        .collect::<Result<Vec<_>, _>>()?;
663    Ok(rows)
664}
665
666/// List facts within a namespace.
667pub fn list_facts(
668    conn: &Connection,
669    namespace: &str,
670    limit: usize,
671    offset: usize,
672) -> Result<Vec<Fact>, MemoryError> {
673    let mut stmt = conn.prepare(
674        "SELECT id, namespace, content, source, created_at, updated_at, metadata
675         FROM facts
676         WHERE namespace = ?1
677         ORDER BY updated_at DESC
678         LIMIT ?2 OFFSET ?3",
679    )?;
680
681    let facts = stmt
682        .query_map(params![namespace, limit as i64, offset as i64], |row| {
683            Ok((
684                row.get::<_, String>(0)?,
685                row.get::<_, String>(1)?,
686                row.get::<_, String>(2)?,
687                row.get::<_, Option<String>>(3)?,
688                row.get::<_, String>(4)?,
689                row.get::<_, String>(5)?,
690                row.get::<_, Option<String>>(6)?,
691            ))
692        })?
693        .collect::<Result<Vec<_>, _>>()?
694        .into_iter()
695        .map(
696            |(id, namespace, content, source, created_at, updated_at, metadata_raw)| {
697                Ok(Fact {
698                    metadata: parse_optional_json(
699                        "facts",
700                        &id,
701                        "metadata",
702                        metadata_raw.as_deref(),
703                    )?,
704                    id,
705                    namespace,
706                    content,
707                    source,
708                    created_at,
709                    updated_at,
710                })
711            },
712        )
713        .collect::<Result<Vec<_>, MemoryError>>()?;
714
715    Ok(facts)
716}
717
718impl MemoryStore {
719    /// Store a fact with automatic embedding. Returns the fact ID (UUID v4).
720    pub async fn add_fact(
721        &self,
722        namespace: &str,
723        content: &str,
724        source: Option<&str>,
725        metadata: Option<serde_json::Value>,
726    ) -> Result<String, MemoryError> {
727        self.add_fact_with_trace(namespace, content, source, metadata, None)
728            .await
729    }
730
731    /// Store a fact with automatic embedding and optional trace metadata.
732    pub async fn add_fact_with_trace(
733        &self,
734        namespace: &str,
735        content: &str,
736        source: Option<&str>,
737        metadata: Option<serde_json::Value>,
738        trace_ctx: Option<&TraceCtx>,
739    ) -> Result<String, MemoryError> {
740        self.validate_content("fact.content", content)?;
741
742        // Dedup: check if a fact with the same content already exists.
743        // This prevents the 4-5% DB bloat from duplicate ingestion.
744        let ns_check = namespace.to_string();
745        let ct_check = content.to_string();
746        let existing_id = self
747            .with_read_conn(move |conn| {
748                let result: Option<String> = conn
749                    .query_row(
750                        "SELECT id FROM facts WHERE content = ?1 AND namespace = ?2 LIMIT 1",
751                        rusqlite::params![&ct_check, &ns_check],
752                        |row| row.get::<_, String>(0),
753                    )
754                    .ok();
755                Ok(result)
756            })
757            .await?;
758
759        if let Some(id) = existing_id {
760            return Ok(id);
761        }
762
763        let embedding = self.embed_text_internal(content).await?;
764        self.validate_embedding_dimensions(&embedding)?;
765        let embedding_bytes = db::embedding_to_bytes(&embedding);
766        let fact_id = uuid::Uuid::new_v4().to_string();
767        let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
768
769        let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
770        // INTENTIONAL: q8 quantization is an optional search optimization; missing q8 is non-fatal
771        let q8_bytes = quantizer
772            .quantize(&embedding)
773            .map(|qv| quantize::pack_quantized(&qv))
774            .ok();
775
776        let ns = namespace.to_string();
777        let ct = content.to_string();
778        let fid = fact_id.clone();
779        let src = source.map(|s| s.to_string());
780        let meta = merge_trace_ctx(metadata, trace_ctx);
781        self.with_write_conn(move |conn| {
782            let current_count: usize = conn.query_row(
783                "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
784                rusqlite::params![&ns],
785                |row| row.get(0),
786            )?;
787            if current_count >= max_facts_per_namespace {
788                return Err(MemoryError::NamespaceFull {
789                    namespace: ns.clone(),
790                    count: current_count,
791                    limit: max_facts_per_namespace,
792                });
793            }
794            insert_fact_with_fts_q8(
795                conn,
796                &fid,
797                &ns,
798                &ct,
799                &embedding_bytes,
800                q8_bytes.as_deref(),
801                src.as_deref(),
802                meta.as_ref(),
803            )
804        })
805        .await?;
806
807        #[cfg(feature = "hnsw")]
808        self.sync_pending_hnsw_ops_best_effort("add_fact").await;
809
810        Ok(fact_id)
811    }
812
813    /// Store a fact with a pre-computed embedding.
814    pub async fn add_fact_with_embedding(
815        &self,
816        namespace: &str,
817        content: &str,
818        embedding: &[f32],
819        source: Option<&str>,
820        metadata: Option<serde_json::Value>,
821    ) -> Result<String, MemoryError> {
822        self.add_fact_with_embedding_and_trace(
823            namespace, content, embedding, source, metadata, None,
824        )
825        .await
826    }
827
828    /// Store a fact with a pre-computed embedding and optional trace metadata.
829    pub async fn add_fact_with_embedding_and_trace(
830        &self,
831        namespace: &str,
832        content: &str,
833        embedding: &[f32],
834        source: Option<&str>,
835        metadata: Option<serde_json::Value>,
836        trace_ctx: Option<&TraceCtx>,
837    ) -> Result<String, MemoryError> {
838        self.validate_content("fact.content", content)?;
839        self.validate_embedding_dimensions(embedding)?;
840        let embedding_bytes = db::embedding_to_bytes(embedding);
841        let fact_id = uuid::Uuid::new_v4().to_string();
842        let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
843
844        let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
845        // INTENTIONAL: q8 quantization is an optional search optimization; missing q8 is non-fatal
846        let q8_bytes = quantizer
847            .quantize(embedding)
848            .map(|qv| quantize::pack_quantized(&qv))
849            .ok();
850
851        let ns = namespace.to_string();
852        let ct = content.to_string();
853        let fid = fact_id.clone();
854        let src = source.map(|s| s.to_string());
855        let meta = merge_trace_ctx(metadata, trace_ctx);
856        self.with_write_conn(move |conn| {
857            let current_count: usize = conn.query_row(
858                "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
859                rusqlite::params![&ns],
860                |row| row.get(0),
861            )?;
862            if current_count >= max_facts_per_namespace {
863                return Err(MemoryError::NamespaceFull {
864                    namespace: ns.clone(),
865                    count: current_count,
866                    limit: max_facts_per_namespace,
867                });
868            }
869            insert_fact_with_fts_q8(
870                conn,
871                &fid,
872                &ns,
873                &ct,
874                &embedding_bytes,
875                q8_bytes.as_deref(),
876                src.as_deref(),
877                meta.as_ref(),
878            )
879        })
880        .await?;
881
882        #[cfg(feature = "hnsw")]
883        self.sync_pending_hnsw_ops_best_effort("add_fact_with_embedding")
884            .await;
885
886        Ok(fact_id)
887    }
888
889    /// **DANGER**: This physically mutates/deletes a truth-bearing row.
890    /// This is admin-only and gated behind the `admin-ops` feature.
891    /// Default agent-facing APIs should use supersession (add a new fact
892    /// with a supersession link) instead of hard delete/update.
893    #[cfg(feature = "admin-ops")]
894    pub async fn update_fact(&self, fact_id: &str, content: &str) -> Result<(), MemoryError> {
895        self.validate_content("fact.content", content)?;
896        let embedding = self.embed_text_internal(content).await?;
897        self.validate_embedding_dimensions(&embedding)?;
898        let embedding_bytes = db::embedding_to_bytes(&embedding);
899        // INTENTIONAL: q8 quantization is an optional search optimization; missing q8 is non-fatal
900        let q8_bytes = Quantizer::new(self.inner.config.embedding.dimensions)
901            .quantize(&embedding)
902            .map(|qv| quantize::pack_quantized(&qv))
903            .ok();
904
905        let fid = fact_id.to_string();
906        let ct = content.to_string();
907        self.with_write_conn(move |conn| {
908            update_fact_with_fts(conn, &fid, &ct, &embedding_bytes, q8_bytes.as_deref())
909        })
910        .await?;
911
912        #[cfg(feature = "hnsw")]
913        self.sync_pending_hnsw_ops_best_effort("update_fact").await;
914
915        self.clear_search_cache();
916
917        Ok(())
918    }
919
920    /// **DANGER**: This physically mutates/deletes a truth-bearing row.
921    /// This is admin-only and gated behind the `admin-ops` feature.
922    /// Default agent-facing APIs should use supersession (add a new fact
923    /// with a supersession link) instead of hard delete/update.
924    #[cfg(feature = "admin-ops")]
925    pub async fn delete_fact(&self, fact_id: &str) -> Result<(), MemoryError> {
926        let fid = fact_id.to_string();
927        self.with_write_conn(move |conn| delete_fact_with_fts(conn, &fid))
928            .await?;
929
930        #[cfg(feature = "hnsw")]
931        self.sync_pending_hnsw_ops_best_effort("delete_fact").await;
932
933        self.clear_search_cache();
934
935        Ok(())
936    }
937
938    /// Delete all memory in a namespace and return a per-surface report.
939    pub async fn delete_namespace(
940        &self,
941        namespace: &str,
942    ) -> Result<NamespaceDeleteReport, MemoryError> {
943        let ns = namespace.to_string();
944        let count = self
945            .with_write_conn(move |conn| delete_namespace(conn, &ns))
946            .await?;
947
948        #[cfg(feature = "hnsw")]
949        self.sync_pending_hnsw_ops_best_effort("delete_namespace")
950            .await;
951
952        self.clear_search_cache();
953
954        Ok(count)
955    }
956
957    /// Get a fact by ID.
958    pub async fn get_fact(&self, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
959        let fid = fact_id.to_string();
960        self.with_read_conn(move |conn| get_fact(conn, &fid)).await
961    }
962
963    /// Get a fact's embedding vector.
964    pub async fn get_fact_embedding(&self, fact_id: &str) -> Result<Option<Vec<f32>>, MemoryError> {
965        let fid = fact_id.to_string();
966        self.with_read_conn(move |conn| get_fact_embedding(conn, &fid))
967            .await
968    }
969
970    /// List all facts in a namespace.
971    pub async fn list_facts(
972        &self,
973        namespace: &str,
974        limit: usize,
975        offset: usize,
976    ) -> Result<Vec<Fact>, MemoryError> {
977        let ns = namespace.to_string();
978        self.with_read_conn(move |conn| list_facts(conn, &ns, limit, offset))
979            .await
980    }
981
982    /// List the distinct namespaces that currently contain facts.
983    pub async fn list_fact_namespaces(&self) -> Result<Vec<String>, MemoryError> {
984        self.with_read_conn(move |conn| list_fact_namespaces(conn)).await
985    }
986}