Skip to main content

mnemo_core/storage/
duckdb.rs

1use std::path::Path;
2use std::sync::Arc;
3use tokio::sync::Mutex;
4
5use crate::error::{Error, Result};
6use crate::model::acl::{Acl, Permission};
7use crate::model::agent_profile::AgentProfile;
8use crate::model::checkpoint::Checkpoint;
9use crate::model::delegation::{Delegation, DelegationScope};
10use crate::model::embedding_baseline::EmbeddingBaseline;
11use crate::model::event::AgentEvent;
12use crate::model::memory::MemoryRecord;
13use crate::model::relation::Relation;
14use crate::storage::{MemoryFilter, StorageBackend};
15use uuid::Uuid;
16
17pub struct DuckDbStorage {
18    conn: Arc<Mutex<duckdb::Connection>>,
19}
20
21impl DuckDbStorage {
22    pub fn open(path: &Path) -> Result<Self> {
23        let conn = duckdb::Connection::open(path)?;
24        super::migrations::run_migrations(&conn)?;
25        Ok(Self {
26            conn: Arc::new(Mutex::new(conn)),
27        })
28    }
29
30    pub fn open_in_memory() -> Result<Self> {
31        let conn = duckdb::Connection::open_in_memory()?;
32        super::migrations::run_migrations(&conn)?;
33        Ok(Self {
34            conn: Arc::new(Mutex::new(conn)),
35        })
36    }
37}
38
39fn serialize_embedding(embedding: &Option<Vec<f32>>) -> Option<Vec<u8>> {
40    embedding
41        .as_ref()
42        .map(|v| v.iter().flat_map(|f| f.to_le_bytes()).collect())
43}
44
45fn deserialize_embedding(blob: Option<Vec<u8>>) -> Option<Vec<f32>> {
46    blob.map(|bytes| {
47        bytes
48            .chunks_exact(4)
49            .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
50            .collect()
51    })
52}
53
54fn row_to_memory(row: &duckdb::Row<'_>) -> duckdb::Result<MemoryRecord> {
55    let id_str: String = row.get(0)?;
56    let tags_json: Option<String> = row.get(6)?;
57    let metadata_json: Option<String> = row.get(7)?;
58    let embedding_blob: Option<Vec<u8>> = row.get(8)?;
59    let content_hash: Vec<u8> = row.get(9)?;
60    let prev_hash: Option<Vec<u8>> = row.get(10)?;
61
62    let memory_type_str: String = row.get(3)?;
63    let scope_str: String = row.get(4)?;
64    let source_type_str: String = row.get(11)?;
65    let consolidation_state_str: String = row.get(13)?;
66
67    Ok(MemoryRecord {
68        id: Uuid::parse_str(&id_str)
69            .map_err(|e| duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e)))?,
70        agent_id: row.get(1)?,
71        content: row.get(2)?,
72        memory_type: memory_type_str.parse()
73            .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(3, duckdb::types::Type::Text, e.to_string().into()))?,
74        scope: scope_str.parse()
75            .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(4, duckdb::types::Type::Text, e.to_string().into()))?,
76        importance: row.get(5)?,
77        tags: match tags_json {
78            Some(ref s) => serde_json::from_str(s).unwrap_or_else(|e| {
79                tracing::warn!(id = %id_str, error = %e, raw = %s, "corrupted tags JSON, defaulting to empty");
80                vec![]
81            }),
82            None => vec![],
83        },
84        metadata: match metadata_json {
85            Some(ref s) => serde_json::from_str(s).unwrap_or_else(|e| {
86                tracing::warn!(id = %id_str, error = %e, "corrupted metadata JSON, defaulting to empty");
87                serde_json::Value::Object(serde_json::Map::new())
88            }),
89            None => serde_json::Value::Object(serde_json::Map::new()),
90        },
91        embedding: deserialize_embedding(embedding_blob),
92        content_hash,
93        prev_hash,
94        source_type: source_type_str.parse()
95            .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(11, duckdb::types::Type::Text, e.to_string().into()))?,
96        source_id: row.get(12)?,
97        consolidation_state: consolidation_state_str.parse()
98            .map_err(|e: Error| duckdb::Error::FromSqlConversionFailure(13, duckdb::types::Type::Text, e.to_string().into()))?,
99        access_count: u64::try_from(row.get::<_, i64>(14)?).unwrap_or(0),
100        org_id: row.get(15)?,
101        thread_id: row.get(16)?,
102        created_at: row.get(17)?,
103        updated_at: row.get(18)?,
104        last_accessed_at: row.get(19)?,
105        expires_at: row.get(20)?,
106        deleted_at: row.get(21)?,
107        decay_rate: row.get(22)?,
108        created_by: row.get(23)?,
109        version: u32::try_from(row.get::<_, i32>(24)?).unwrap_or(1),
110        prev_version_id: match row.get::<_, Option<String>>(25)? {
111            Some(s) => Uuid::parse_str(&s).map_err(|e| {
112                tracing::warn!(memory_id = %id_str, error = %e, "corrupted prev_version_id UUID");
113                e
114            }).ok(),
115            None => None,
116        },
117        quarantined: row.get::<_, bool>(26)?,
118        quarantine_reason: row.get(27)?,
119        decay_function: row.get(28).unwrap_or(None),
120    })
121}
122
123#[async_trait::async_trait]
124impl StorageBackend for DuckDbStorage {
125    fn backend_name(&self) -> &'static str {
126        "duckdb"
127    }
128
129    async fn insert_memory(&self, record: &MemoryRecord) -> Result<()> {
130        let conn = self.conn.lock().await;
131        let tags_json = serde_json::to_string(&record.tags)?;
132        let metadata_json = serde_json::to_string(&record.metadata)?;
133        let embedding_blob = serialize_embedding(&record.embedding);
134
135        conn.execute(
136            "INSERT INTO memories (id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
137            duckdb::params![
138                record.id.to_string(),
139                record.agent_id,
140                record.content,
141                record.memory_type.to_string(),
142                record.scope.to_string(),
143                record.importance,
144                tags_json,
145                metadata_json,
146                embedding_blob,
147                record.content_hash,
148                record.prev_hash,
149                record.source_type.to_string(),
150                record.source_id,
151                record.consolidation_state.to_string(),
152                record.access_count as i64,
153                record.org_id,
154                record.thread_id,
155                record.created_at,
156                record.updated_at,
157                record.last_accessed_at,
158                record.expires_at,
159                record.deleted_at,
160                record.decay_rate,
161                record.created_by,
162                record.version as i32,
163                record.prev_version_id.map(|id| id.to_string()),
164                record.quarantined,
165                record.quarantine_reason,
166                record.decay_function,
167            ],
168        )?;
169        Ok(())
170    }
171
172    async fn get_memory(&self, id: Uuid) -> Result<Option<MemoryRecord>> {
173        let conn = self.conn.lock().await;
174        let mut stmt = conn.prepare(
175            "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE id = ?",
176        )?;
177        let result = stmt.query_row([id.to_string()], row_to_memory);
178        match result {
179            Ok(record) => Ok(Some(record)),
180            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
181            Err(e) => Err(Error::Storage(e.to_string())),
182        }
183    }
184
185    async fn update_memory(&self, record: &MemoryRecord) -> Result<()> {
186        let conn = self.conn.lock().await;
187        let tags_json = serde_json::to_string(&record.tags)?;
188        let metadata_json = serde_json::to_string(&record.metadata)?;
189        let embedding_blob = serialize_embedding(&record.embedding);
190
191        let affected = conn.execute(
192            "UPDATE memories SET agent_id=?, content=?, memory_type=?, scope=?, importance=?, tags=?, metadata=?, embedding=?, content_hash=?, prev_hash=?, source_type=?, source_id=?, consolidation_state=?, access_count=?, org_id=?, thread_id=?, updated_at=?, last_accessed_at=?, expires_at=?, deleted_at=?, decay_rate=?, created_by=?, version=?, prev_version_id=?, quarantined=?, quarantine_reason=?, decay_function=? WHERE id=?",
193            duckdb::params![
194                record.agent_id,
195                record.content,
196                record.memory_type.to_string(),
197                record.scope.to_string(),
198                record.importance,
199                tags_json,
200                metadata_json,
201                embedding_blob,
202                record.content_hash,
203                record.prev_hash,
204                record.source_type.to_string(),
205                record.source_id,
206                record.consolidation_state.to_string(),
207                record.access_count as i64,
208                record.org_id,
209                record.thread_id,
210                record.updated_at,
211                record.last_accessed_at,
212                record.expires_at,
213                record.deleted_at,
214                record.decay_rate,
215                record.created_by,
216                record.version as i32,
217                record.prev_version_id.map(|id| id.to_string()),
218                record.quarantined,
219                record.quarantine_reason,
220                record.decay_function,
221                record.id.to_string(),
222            ],
223        )?;
224        if affected == 0 {
225            return Err(Error::NotFound(format!("memory {} not found", record.id)));
226        }
227        Ok(())
228    }
229
230    async fn soft_delete_memory(&self, id: Uuid) -> Result<()> {
231        let conn = self.conn.lock().await;
232        let now = chrono::Utc::now().to_rfc3339();
233        let affected = conn.execute(
234            "UPDATE memories SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL",
235            duckdb::params![now, now, id.to_string()],
236        )?;
237        if affected == 0 {
238            return Err(Error::NotFound(format!(
239                "memory {id} not found or already deleted"
240            )));
241        }
242        Ok(())
243    }
244
245    async fn hard_delete_memory(&self, id: Uuid) -> Result<()> {
246        let conn = self.conn.lock().await;
247        let affected = conn.execute(
248            "DELETE FROM memories WHERE id = ?",
249            duckdb::params![id.to_string()],
250        )?;
251        if affected == 0 {
252            return Err(Error::NotFound(format!("memory {id} not found")));
253        }
254        // Also clean up ACLs
255        conn.execute(
256            "DELETE FROM acls WHERE memory_id = ?",
257            duckdb::params![id.to_string()],
258        )?;
259        Ok(())
260    }
261
262    async fn list_memories(
263        &self,
264        filter: &MemoryFilter,
265        limit: usize,
266        offset: usize,
267    ) -> Result<Vec<MemoryRecord>> {
268        let conn = self.conn.lock().await;
269        let mut conditions = Vec::new();
270        let mut params: Vec<Box<dyn duckdb::ToSql>> = Vec::new();
271
272        if !filter.include_deleted {
273            conditions.push("deleted_at IS NULL".to_string());
274        }
275
276        if let Some(ref agent_id) = filter.agent_id {
277            conditions.push(format!("agent_id = ${}", params.len() + 1));
278            params.push(Box::new(agent_id.clone()));
279        }
280
281        if let Some(memory_type) = filter.memory_type {
282            conditions.push(format!("memory_type = ${}", params.len() + 1));
283            params.push(Box::new(memory_type.to_string()));
284        }
285
286        if let Some(scope) = filter.scope {
287            conditions.push(format!("scope = ${}", params.len() + 1));
288            params.push(Box::new(scope.to_string()));
289        }
290
291        if let Some(min_importance) = filter.min_importance {
292            conditions.push(format!("importance >= ${}", params.len() + 1));
293            params.push(Box::new(min_importance));
294        }
295
296        if let Some(ref org_id) = filter.org_id {
297            conditions.push(format!("org_id = ${}", params.len() + 1));
298            params.push(Box::new(org_id.clone()));
299        }
300
301        if let Some(ref thread_id) = filter.thread_id {
302            conditions.push(format!("thread_id = ${}", params.len() + 1));
303            params.push(Box::new(thread_id.clone()));
304        }
305
306        let where_clause = if conditions.is_empty() {
307            String::new()
308        } else {
309            format!("WHERE {}", conditions.join(" AND "))
310        };
311
312        let sql = format!(
313            "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories {where_clause} ORDER BY created_at DESC LIMIT {limit} OFFSET {offset}"
314        );
315
316        let mut stmt = conn.prepare(&sql)?;
317        let param_refs: Vec<&dyn duckdb::ToSql> = params.iter().map(|p| p.as_ref()).collect();
318        let rows = stmt.query_map(param_refs.as_slice(), row_to_memory)?;
319
320        let mut results = Vec::new();
321        for row in rows {
322            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
323        }
324        Ok(results)
325    }
326
327    async fn touch_memory(&self, id: Uuid) -> Result<()> {
328        let conn = self.conn.lock().await;
329        let now = chrono::Utc::now().to_rfc3339();
330        conn.execute(
331            "UPDATE memories SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?",
332            duckdb::params![now, id.to_string()],
333        )?;
334        Ok(())
335    }
336
337    async fn insert_acl(&self, acl: &Acl) -> Result<()> {
338        let conn = self.conn.lock().await;
339        conn.execute(
340            "INSERT INTO acls (id, memory_id, principal_type, principal_id, permission, granted_by, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
341            duckdb::params![
342                acl.id.to_string(),
343                acl.memory_id.to_string(),
344                acl.principal_type.to_string(),
345                acl.principal_id,
346                acl.permission.to_string(),
347                acl.granted_by,
348                acl.created_at,
349                acl.expires_at,
350            ],
351        )?;
352        Ok(())
353    }
354
355    async fn check_permission(
356        &self,
357        memory_id: Uuid,
358        principal_id: &str,
359        required: Permission,
360    ) -> Result<bool> {
361        // Do all DuckDB work in one block, then release the lock before delegation check
362        let acl_result = {
363            let conn = self.conn.lock().await;
364
365            // Check if the principal is the owner (agent_id matches)
366            let mut stmt = conn.prepare("SELECT agent_id FROM memories WHERE id = ?")?;
367            let owner_result =
368                stmt.query_row([memory_id.to_string()], |row| row.get::<_, String>(0));
369            match owner_result {
370                Ok(owner) if owner == principal_id => return Ok(true),
371                Err(duckdb::Error::QueryReturnedNoRows) => {
372                    return Err(Error::NotFound(format!("memory {memory_id} not found")));
373                }
374                _ => {}
375            }
376
377            // Check ACLs
378            let now = chrono::Utc::now().to_rfc3339();
379            let mut stmt = conn.prepare(
380                "SELECT permission FROM acls WHERE memory_id = ? AND principal_id = ? AND (expires_at IS NULL OR expires_at > ?)",
381            )?;
382            let rows = stmt.query_map(
383                duckdb::params![memory_id.to_string(), principal_id, now.clone()],
384                |row| row.get::<_, String>(0),
385            )?;
386
387            let mut perms: Vec<String> = Vec::new();
388            for row in rows {
389                perms.push(row.map_err(|e| Error::Storage(e.to_string()))?);
390            }
391
392            // Check public ACLs
393            let mut stmt = conn.prepare(
394                "SELECT permission FROM acls WHERE memory_id = ? AND principal_type = 'public' AND (expires_at IS NULL OR expires_at > ?)",
395            )?;
396            let rows = stmt.query_map(duckdb::params![memory_id.to_string(), now], |row| {
397                row.get::<_, String>(0)
398            })?;
399
400            for row in rows {
401                perms.push(row.map_err(|e| Error::Storage(e.to_string()))?);
402            }
403
404            perms
405        }; // conn lock dropped here
406
407        for perm_str in &acl_result {
408            if let Ok(perm) = perm_str.parse::<Permission>()
409                && perm.satisfies(required)
410            {
411                return Ok(true);
412            }
413        }
414
415        // Check delegations (conn lock is released)
416        if self
417            .check_delegation(principal_id, memory_id, required)
418            .await?
419        {
420            return Ok(true);
421        }
422
423        Ok(false)
424    }
425
426    async fn insert_relation(&self, relation: &Relation) -> Result<()> {
427        let conn = self.conn.lock().await;
428        conn.execute(
429            "INSERT INTO relations (id, source_id, target_id, relation_type, weight, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
430            duckdb::params![
431                relation.id.to_string(),
432                relation.source_id.to_string(),
433                relation.target_id.to_string(),
434                relation.relation_type,
435                relation.weight,
436                serde_json::to_string(&relation.metadata)?,
437                relation.created_at,
438            ],
439        )?;
440        Ok(())
441    }
442
443    async fn get_relations_from(&self, source_id: Uuid) -> Result<Vec<Relation>> {
444        let conn = self.conn.lock().await;
445        let mut stmt = conn.prepare(
446            "SELECT id, source_id, target_id, relation_type, weight, metadata, created_at FROM relations WHERE source_id = ?",
447        )?;
448        let rows = stmt.query_map([source_id.to_string()], row_to_relation)?;
449        let mut results = Vec::new();
450        for row in rows {
451            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
452        }
453        Ok(results)
454    }
455
456    async fn get_relations_to(&self, target_id: Uuid) -> Result<Vec<Relation>> {
457        let conn = self.conn.lock().await;
458        let mut stmt = conn.prepare(
459            "SELECT id, source_id, target_id, relation_type, weight, metadata, created_at FROM relations WHERE target_id = ?",
460        )?;
461        let rows = stmt.query_map([target_id.to_string()], row_to_relation)?;
462        let mut results = Vec::new();
463        for row in rows {
464            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
465        }
466        Ok(results)
467    }
468
469    async fn delete_relation(&self, id: Uuid) -> Result<()> {
470        let conn = self.conn.lock().await;
471        let affected = conn.execute(
472            "DELETE FROM relations WHERE id = ?",
473            duckdb::params![id.to_string()],
474        )?;
475        if affected == 0 {
476            return Err(Error::NotFound(format!("relation {id} not found")));
477        }
478        Ok(())
479    }
480
481    async fn get_latest_memory_hash(
482        &self,
483        agent_id: &str,
484        thread_id: Option<&str>,
485    ) -> Result<Option<Vec<u8>>> {
486        let conn = self.conn.lock().await;
487        let (sql, result) = if let Some(tid) = thread_id {
488            let mut stmt = conn.prepare(
489                "SELECT content_hash FROM memories WHERE agent_id = ? AND thread_id = ? AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1",
490            )?;
491            let r = stmt.query_row(duckdb::params![agent_id, tid], |row| {
492                row.get::<_, Vec<u8>>(0)
493            });
494            ((), r)
495        } else {
496            let mut stmt = conn.prepare(
497                "SELECT content_hash FROM memories WHERE agent_id = ? AND thread_id IS NULL AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1",
498            )?;
499            let r = stmt.query_row(duckdb::params![agent_id], |row| row.get::<_, Vec<u8>>(0));
500            ((), r)
501        };
502        let _ = sql;
503        match result {
504            Ok(hash) => Ok(Some(hash)),
505            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
506            Err(e) => Err(Error::Storage(e.to_string())),
507        }
508    }
509
510    async fn get_latest_event_hash(
511        &self,
512        agent_id: &str,
513        thread_id: Option<&str>,
514    ) -> Result<Option<Vec<u8>>> {
515        let conn = self.conn.lock().await;
516        let result = if let Some(tid) = thread_id {
517            let mut stmt = conn.prepare(
518                "SELECT content_hash FROM agent_events WHERE agent_id = ? AND thread_id = ? ORDER BY timestamp DESC LIMIT 1",
519            )?;
520            stmt.query_row(duckdb::params![agent_id, tid], |row| {
521                row.get::<_, Vec<u8>>(0)
522            })
523        } else {
524            let mut stmt = conn.prepare(
525                "SELECT content_hash FROM agent_events WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 1",
526            )?;
527            stmt.query_row(duckdb::params![agent_id], |row| row.get::<_, Vec<u8>>(0))
528        };
529        match result {
530            Ok(hash) => Ok(Some(hash)),
531            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
532            Err(e) => Err(Error::Storage(e.to_string())),
533        }
534    }
535
536    async fn get_sync_watermark(&self, key: &str) -> Result<Option<String>> {
537        let conn = self.conn.lock().await;
538        let mut stmt = conn.prepare("SELECT value FROM sync_metadata WHERE key = ?")?;
539        let result = stmt.query_row(duckdb::params![key], |row| row.get::<_, String>(0));
540        match result {
541            Ok(value) => Ok(Some(value)),
542            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
543            Err(e) => Err(Error::Storage(e.to_string())),
544        }
545    }
546
547    async fn set_sync_watermark(&self, key: &str, value: &str) -> Result<()> {
548        let conn = self.conn.lock().await;
549        let now = chrono::Utc::now().to_rfc3339();
550        // Try update first, then insert
551        let affected = conn.execute(
552            "UPDATE sync_metadata SET value = ?, updated_at = ? WHERE key = ?",
553            duckdb::params![value, now, key],
554        )?;
555        if affected == 0 {
556            conn.execute(
557                "INSERT INTO sync_metadata (key, value, updated_at) VALUES (?, ?, ?)",
558                duckdb::params![key, value, now],
559            )?;
560        }
561        Ok(())
562    }
563
564    async fn list_accessible_memory_ids(&self, agent_id: &str, limit: usize) -> Result<Vec<Uuid>> {
565        let conn = self.conn.lock().await;
566        let now = chrono::Utc::now().to_rfc3339();
567        let mut stmt = conn.prepare(
568            "SELECT id FROM memories WHERE (agent_id = ? OR scope = 'public' OR id IN (SELECT memory_id FROM acls WHERE principal_id = ? AND (expires_at IS NULL OR expires_at > ?))) AND deleted_at IS NULL LIMIT ?",
569        )?;
570        let rows = stmt.query_map(
571            duckdb::params![agent_id, agent_id, now, limit as i64],
572            |row| row.get::<_, String>(0),
573        )?;
574        let mut ids = Vec::new();
575        for row in rows {
576            let id_str = row.map_err(|e| Error::Storage(e.to_string()))?;
577            ids.push(Uuid::parse_str(&id_str).map_err(|e| Error::Storage(e.to_string()))?);
578        }
579        Ok(ids)
580    }
581
582    async fn insert_event(&self, event: &AgentEvent) -> Result<()> {
583        let conn = self.conn.lock().await;
584        let payload_json = serde_json::to_string(&event.payload)?;
585        let embedding_blob = serialize_embedding(&event.embedding);
586        conn.execute(
587            "INSERT INTO agent_events (id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
588            duckdb::params![
589                event.id.to_string(),
590                event.agent_id,
591                event.thread_id,
592                event.run_id,
593                event.parent_event_id.map(|id| id.to_string()),
594                event.event_type.to_string(),
595                payload_json,
596                event.trace_id,
597                event.span_id,
598                event.model,
599                event.tokens_input,
600                event.tokens_output,
601                event.latency_ms,
602                event.cost_usd,
603                event.timestamp,
604                event.logical_clock,
605                event.content_hash,
606                event.prev_hash,
607                embedding_blob,
608            ],
609        )?;
610        Ok(())
611    }
612
613    async fn list_events(
614        &self,
615        agent_id: &str,
616        limit: usize,
617        offset: usize,
618    ) -> Result<Vec<AgentEvent>> {
619        let conn = self.conn.lock().await;
620        let mut stmt = conn.prepare(
621            "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE agent_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
622        )?;
623        let rows = stmt.query_map(
624            duckdb::params![agent_id, limit as i64, offset as i64],
625            row_to_event,
626        )?;
627        let mut results = Vec::new();
628        for row in rows {
629            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
630        }
631        Ok(results)
632    }
633
634    async fn get_events_by_thread(&self, thread_id: &str, limit: usize) -> Result<Vec<AgentEvent>> {
635        let conn = self.conn.lock().await;
636        let mut stmt = conn.prepare(
637            "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE thread_id = ? ORDER BY timestamp ASC LIMIT ?",
638        )?;
639        let rows = stmt.query_map(duckdb::params![thread_id, limit as i64], row_to_event)?;
640        let mut results = Vec::new();
641        for row in rows {
642            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
643        }
644        Ok(results)
645    }
646
647    async fn get_event(&self, id: Uuid) -> Result<Option<AgentEvent>> {
648        let conn = self.conn.lock().await;
649        let mut stmt = conn.prepare(
650            "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE id = ?",
651        )?;
652        let result = stmt.query_row([id.to_string()], row_to_event);
653        match result {
654            Ok(event) => Ok(Some(event)),
655            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
656            Err(e) => Err(Error::Storage(e.to_string())),
657        }
658    }
659
660    async fn list_child_events(
661        &self,
662        parent_event_id: Uuid,
663        limit: usize,
664    ) -> Result<Vec<AgentEvent>> {
665        let conn = self.conn.lock().await;
666        let mut stmt = conn.prepare(
667            "SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type, payload, trace_id, span_id, model, tokens_input, tokens_output, latency_ms, cost_usd, timestamp, logical_clock, content_hash, prev_hash, embedding FROM agent_events WHERE parent_event_id = ? ORDER BY timestamp ASC LIMIT ?",
668        )?;
669        let rows = stmt.query_map(
670            duckdb::params![parent_event_id.to_string(), limit as i64],
671            row_to_event,
672        )?;
673        let mut results = Vec::new();
674        for row in rows {
675            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
676        }
677        Ok(results)
678    }
679
680    async fn list_memories_by_agent_ordered(
681        &self,
682        agent_id: &str,
683        thread_id: Option<&str>,
684        limit: usize,
685    ) -> Result<Vec<MemoryRecord>> {
686        let conn = self.conn.lock().await;
687        let (result,) = if let Some(tid) = thread_id {
688            let mut stmt = conn.prepare(
689                "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE agent_id = ? AND thread_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT ?",
690            )?;
691            let rows =
692                stmt.query_map(duckdb::params![agent_id, tid, limit as i64], row_to_memory)?;
693            let mut results = Vec::new();
694            for row in rows {
695                results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
696            }
697            (results,)
698        } else {
699            let mut stmt = conn.prepare(
700                "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE agent_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT ?",
701            )?;
702            let rows = stmt.query_map(duckdb::params![agent_id, limit as i64], row_to_memory)?;
703            let mut results = Vec::new();
704            for row in rows {
705                results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
706            }
707            (results,)
708        };
709        Ok(result)
710    }
711
712    async fn list_memories_since(
713        &self,
714        updated_after: &str,
715        limit: usize,
716    ) -> Result<Vec<MemoryRecord>> {
717        let conn = self.conn.lock().await;
718        let mut stmt = conn.prepare(
719            "SELECT id, agent_id, content, memory_type, scope, importance, tags, metadata, embedding, content_hash, prev_hash, source_type, source_id, consolidation_state, access_count, org_id, thread_id, created_at, updated_at, last_accessed_at, expires_at, deleted_at, decay_rate, created_by, version, prev_version_id, quarantined, quarantine_reason, decay_function FROM memories WHERE updated_at > ? ORDER BY updated_at ASC LIMIT ?",
720        )?;
721        let rows = stmt.query_map(duckdb::params![updated_after, limit as i64], row_to_memory)?;
722        let mut results = Vec::new();
723        for row in rows {
724            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
725        }
726        Ok(results)
727    }
728
729    async fn upsert_memory(&self, record: &MemoryRecord) -> Result<()> {
730        // Try update first; if no rows affected, insert
731        match self.update_memory(record).await {
732            Ok(()) => Ok(()),
733            Err(Error::NotFound(_)) => self.insert_memory(record).await,
734            Err(e) => Err(e),
735        }
736    }
737
738    async fn cleanup_expired(&self) -> Result<usize> {
739        let conn = self.conn.lock().await;
740        let now = chrono::Utc::now().to_rfc3339();
741        let affected = conn.execute(
742            "UPDATE memories SET deleted_at = ? WHERE expires_at IS NOT NULL AND expires_at < ? AND deleted_at IS NULL",
743            duckdb::params![now.clone(), now],
744        )?;
745        Ok(affected)
746    }
747
748    async fn insert_delegation(&self, d: &Delegation) -> Result<()> {
749        let conn = self.conn.lock().await;
750        let scope_type = d.scope.to_string();
751        let scope_value = match &d.scope {
752            DelegationScope::AllMemories => serde_json::Value::Null,
753            DelegationScope::ByTag(tags) => serde_json::json!(tags),
754            DelegationScope::ByMemoryId(ids) => {
755                serde_json::json!(ids.iter().map(|id| id.to_string()).collect::<Vec<_>>())
756            }
757        };
758        let scope_value_json = serde_json::to_string(&scope_value)?;
759
760        conn.execute(
761            "INSERT INTO delegations (id, delegator_id, delegate_id, permission, scope_type, scope_value, max_depth, current_depth, parent_delegation_id, created_at, expires_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
762            duckdb::params![
763                d.id.to_string(),
764                d.delegator_id,
765                d.delegate_id,
766                d.permission.to_string(),
767                scope_type,
768                scope_value_json,
769                d.max_depth as i32,
770                d.current_depth as i32,
771                d.parent_delegation_id.map(|id| id.to_string()),
772                d.created_at,
773                d.expires_at,
774                d.revoked_at,
775            ],
776        )?;
777        Ok(())
778    }
779
780    async fn list_delegations_for(&self, delegate_id: &str) -> Result<Vec<Delegation>> {
781        let conn = self.conn.lock().await;
782        let now = chrono::Utc::now().to_rfc3339();
783        let mut stmt = conn.prepare(
784            "SELECT id, delegator_id, delegate_id, permission, scope_type, scope_value, max_depth, current_depth, parent_delegation_id, created_at, expires_at, revoked_at FROM delegations WHERE delegate_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)",
785        )?;
786        let rows = stmt.query_map(duckdb::params![delegate_id, now], row_to_delegation)?;
787        let mut results = Vec::new();
788        for row in rows {
789            results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
790        }
791        Ok(results)
792    }
793
794    async fn revoke_delegation(&self, id: Uuid) -> Result<()> {
795        let conn = self.conn.lock().await;
796        let now = chrono::Utc::now().to_rfc3339();
797        let affected = conn.execute(
798            "UPDATE delegations SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL",
799            duckdb::params![now, id.to_string()],
800        )?;
801        if affected == 0 {
802            return Err(Error::NotFound(format!(
803                "delegation {id} not found or already revoked"
804            )));
805        }
806        Ok(())
807    }
808
809    async fn check_delegation(
810        &self,
811        delegate_id: &str,
812        memory_id: Uuid,
813        required: Permission,
814    ) -> Result<bool> {
815        let delegations = self.list_delegations_for(delegate_id).await?;
816        // Get the memory to check scope
817        let memory = match self.get_memory(memory_id).await? {
818            Some(m) => m,
819            None => return Ok(false),
820        };
821
822        for d in &delegations {
823            if !d.permission.satisfies(required) {
824                continue;
825            }
826            match &d.scope {
827                DelegationScope::AllMemories => return Ok(true),
828                DelegationScope::ByMemoryId(ids) => {
829                    if ids.contains(&memory_id) {
830                        return Ok(true);
831                    }
832                }
833                DelegationScope::ByTag(tags) => {
834                    if tags.iter().any(|t| memory.tags.contains(t)) {
835                        return Ok(true);
836                    }
837                }
838            }
839        }
840        Ok(false)
841    }
842
843    async fn insert_or_update_agent_profile(&self, profile: &AgentProfile) -> Result<()> {
844        let conn = self.conn.lock().await;
845        // Try update first, then insert
846        let affected = conn.execute(
847            "UPDATE agent_profiles SET avg_importance = ?, avg_content_length = ?, total_memories = ?, last_updated = ? WHERE agent_id = ?",
848            duckdb::params![
849                profile.avg_importance,
850                profile.avg_content_length,
851                profile.total_memories as i64,
852                profile.last_updated,
853                profile.agent_id,
854            ],
855        )?;
856        if affected == 0 {
857            conn.execute(
858                "INSERT INTO agent_profiles (agent_id, avg_importance, avg_content_length, total_memories, last_updated) VALUES (?, ?, ?, ?, ?)",
859                duckdb::params![
860                    profile.agent_id,
861                    profile.avg_importance,
862                    profile.avg_content_length,
863                    profile.total_memories as i64,
864                    profile.last_updated,
865                ],
866            )?;
867        }
868        Ok(())
869    }
870
871    async fn get_agent_profile(&self, agent_id: &str) -> Result<Option<AgentProfile>> {
872        let conn = self.conn.lock().await;
873        let mut stmt = conn.prepare(
874            "SELECT agent_id, avg_importance, avg_content_length, total_memories, last_updated FROM agent_profiles WHERE agent_id = ?",
875        )?;
876        let result = stmt.query_row([agent_id], |row| {
877            Ok(AgentProfile {
878                agent_id: row.get(0)?,
879                avg_importance: row.get(1)?,
880                avg_content_length: row.get(2)?,
881                total_memories: row.get::<_, i64>(3)? as u64,
882                last_updated: row.get(4)?,
883            })
884        });
885        match result {
886            Ok(profile) => Ok(Some(profile)),
887            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
888            Err(e) => Err(Error::Storage(e.to_string())),
889        }
890    }
891
892    async fn insert_or_update_embedding_baseline(
893        &self,
894        baseline: &EmbeddingBaseline,
895    ) -> Result<()> {
896        let conn = self.conn.lock().await;
897        let mu_json = serde_json::to_string(&baseline.mu)?;
898        let cov_json = serde_json::to_string(&baseline.cov_diag)?;
899        let affected = conn.execute(
900            "UPDATE embedding_baseline SET mu = ?, cov_diag = ?, n = ?, updated_at = ? WHERE agent_id = ?",
901            duckdb::params![
902                mu_json,
903                cov_json,
904                baseline.n as i64,
905                baseline.updated_at,
906                baseline.agent_id,
907            ],
908        )?;
909        if affected == 0 {
910            let mu_json = serde_json::to_string(&baseline.mu)?;
911            let cov_json = serde_json::to_string(&baseline.cov_diag)?;
912            conn.execute(
913                "INSERT INTO embedding_baseline (agent_id, mu, cov_diag, n, updated_at) VALUES (?, ?, ?, ?, ?)",
914                duckdb::params![
915                    baseline.agent_id,
916                    mu_json,
917                    cov_json,
918                    baseline.n as i64,
919                    baseline.updated_at,
920                ],
921            )?;
922        }
923        Ok(())
924    }
925
926    async fn get_embedding_baseline(&self, agent_id: &str) -> Result<Option<EmbeddingBaseline>> {
927        let conn = self.conn.lock().await;
928        let mut stmt = conn.prepare(
929            "SELECT agent_id, mu, cov_diag, n, updated_at FROM embedding_baseline WHERE agent_id = ?",
930        )?;
931        let result: duckdb::Result<(String, String, String, i64, String)> =
932            stmt.query_row([agent_id], |row| {
933                Ok((
934                    row.get(0)?,
935                    row.get(1)?,
936                    row.get(2)?,
937                    row.get(3)?,
938                    row.get(4)?,
939                ))
940            });
941        match result {
942            Ok((agent_id, mu_json, cov_json, n, updated_at)) => {
943                let mu: Vec<f32> = serde_json::from_str(&mu_json)?;
944                let cov_diag: Vec<f32> = serde_json::from_str(&cov_json)?;
945                Ok(Some(EmbeddingBaseline {
946                    agent_id,
947                    mu,
948                    cov_diag,
949                    n: n as u64,
950                    updated_at,
951                }))
952            }
953            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
954            Err(e) => Err(Error::Storage(e.to_string())),
955        }
956    }
957
958    async fn insert_checkpoint(&self, cp: &Checkpoint) -> Result<()> {
959        let conn = self.conn.lock().await;
960        let state_snapshot_json = serde_json::to_string(&cp.state_snapshot)?;
961        let state_diff_json = cp
962            .state_diff
963            .as_ref()
964            .map(serde_json::to_string)
965            .transpose()?;
966        let memory_refs_json = serde_json::to_string(
967            &cp.memory_refs
968                .iter()
969                .map(|id| id.to_string())
970                .collect::<Vec<_>>(),
971        )?;
972        let metadata_json = serde_json::to_string(&cp.metadata)?;
973
974        conn.execute(
975            "INSERT INTO checkpoints (id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
976            duckdb::params![
977                cp.id.to_string(),
978                cp.thread_id,
979                cp.agent_id,
980                cp.parent_id.map(|id| id.to_string()),
981                cp.branch_name,
982                state_snapshot_json,
983                state_diff_json,
984                memory_refs_json,
985                cp.event_cursor.map(|id| id.to_string()),
986                cp.label,
987                cp.created_at,
988                metadata_json,
989            ],
990        )?;
991        Ok(())
992    }
993
994    async fn get_checkpoint(&self, id: Uuid) -> Result<Option<Checkpoint>> {
995        let conn = self.conn.lock().await;
996        let mut stmt = conn.prepare(
997            "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE id = ?",
998        )?;
999        let result = stmt.query_row([id.to_string()], row_to_checkpoint);
1000        match result {
1001            Ok(cp) => Ok(Some(cp)),
1002            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
1003            Err(e) => Err(Error::Storage(e.to_string())),
1004        }
1005    }
1006
1007    async fn list_checkpoints(
1008        &self,
1009        thread_id: &str,
1010        branch: Option<&str>,
1011        limit: usize,
1012    ) -> Result<Vec<Checkpoint>> {
1013        let conn = self.conn.lock().await;
1014        let (sql, rows_result) = if let Some(branch_name) = branch {
1015            let mut stmt = conn.prepare(
1016                "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE thread_id = ? AND branch_name = ? ORDER BY created_at DESC LIMIT ?",
1017            )?;
1018            let rows = stmt.query_map(
1019                duckdb::params![thread_id, branch_name, limit as i64],
1020                row_to_checkpoint,
1021            )?;
1022            let mut results = Vec::new();
1023            for row in rows {
1024                results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
1025            }
1026            ((), Ok(results))
1027        } else {
1028            let mut stmt = conn.prepare(
1029                "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE thread_id = ? ORDER BY created_at DESC LIMIT ?",
1030            )?;
1031            let rows =
1032                stmt.query_map(duckdb::params![thread_id, limit as i64], row_to_checkpoint)?;
1033            let mut results = Vec::new();
1034            for row in rows {
1035                results.push(row.map_err(|e| Error::Storage(e.to_string()))?);
1036            }
1037            ((), Ok(results))
1038        };
1039        let _ = sql;
1040        rows_result
1041    }
1042
1043    async fn get_latest_checkpoint(
1044        &self,
1045        thread_id: &str,
1046        branch: &str,
1047    ) -> Result<Option<Checkpoint>> {
1048        let conn = self.conn.lock().await;
1049        let mut stmt = conn.prepare(
1050            "SELECT id, thread_id, agent_id, parent_id, branch_name, state_snapshot, state_diff, memory_refs, event_cursor, label, created_at, metadata FROM checkpoints WHERE thread_id = ? AND branch_name = ? ORDER BY created_at DESC LIMIT 1",
1051        )?;
1052        let result = stmt.query_row(duckdb::params![thread_id, branch], row_to_checkpoint);
1053        match result {
1054            Ok(cp) => Ok(Some(cp)),
1055            Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
1056            Err(e) => Err(Error::Storage(e.to_string())),
1057        }
1058    }
1059}
1060
1061fn row_to_event(row: &duckdb::Row<'_>) -> duckdb::Result<AgentEvent> {
1062    let id_str: String = row.get(0)?;
1063    let parent_id_str: Option<String> = row.get(4)?;
1064    let payload_json: Option<String> = row.get(6)?;
1065    let event_type_str: String = row.get(5)?;
1066    let content_hash: Vec<u8> = row.get(16)?;
1067    let prev_hash: Option<Vec<u8>> = row.get(17)?;
1068    let embedding_blob: Option<Vec<u8>> = row.get(18).unwrap_or(None);
1069
1070    Ok(AgentEvent {
1071        id: Uuid::parse_str(&id_str).map_err(|e| {
1072            duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1073        })?,
1074        agent_id: row.get(1)?,
1075        thread_id: row.get(2)?,
1076        run_id: row.get(3)?,
1077        parent_event_id: parent_id_str.and_then(|s| Uuid::parse_str(&s).ok()),
1078        event_type: event_type_str.parse().map_err(|e: Error| {
1079            duckdb::Error::FromSqlConversionFailure(
1080                5,
1081                duckdb::types::Type::Text,
1082                e.to_string().into(),
1083            )
1084        })?,
1085        payload: payload_json
1086            .and_then(|s| serde_json::from_str(&s).ok())
1087            .unwrap_or(serde_json::Value::Null),
1088        trace_id: row.get(7)?,
1089        span_id: row.get(8)?,
1090        model: row.get(9)?,
1091        tokens_input: row.get(10)?,
1092        tokens_output: row.get(11)?,
1093        latency_ms: row.get(12)?,
1094        cost_usd: row.get(13)?,
1095        timestamp: row.get(14)?,
1096        logical_clock: row.get(15)?,
1097        content_hash,
1098        prev_hash,
1099        embedding: deserialize_embedding(embedding_blob),
1100    })
1101}
1102
1103fn row_to_checkpoint(row: &duckdb::Row<'_>) -> duckdb::Result<Checkpoint> {
1104    let id_str: String = row.get(0)?;
1105    let parent_id_str: Option<String> = row.get(3)?;
1106    let state_snapshot_json: Option<String> = row.get(5)?;
1107    let state_diff_json: Option<String> = row.get(6)?;
1108    let memory_refs_json: Option<String> = row.get(7)?;
1109    let event_cursor_str: Option<String> = row.get(8)?;
1110    let metadata_json: Option<String> = row.get(11)?;
1111
1112    Ok(Checkpoint {
1113        id: Uuid::parse_str(&id_str).map_err(|e| {
1114            duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1115        })?,
1116        thread_id: row.get(1)?,
1117        agent_id: row.get(2)?,
1118        parent_id: parent_id_str.and_then(|s| Uuid::parse_str(&s).ok()),
1119        branch_name: row.get(4)?,
1120        state_snapshot: state_snapshot_json
1121            .and_then(|s| serde_json::from_str(&s).ok())
1122            .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
1123        state_diff: state_diff_json.and_then(|s| serde_json::from_str(&s).ok()),
1124        memory_refs: memory_refs_json
1125            .and_then(|s| serde_json::from_str::<Vec<String>>(&s).ok())
1126            .map(|v| {
1127                v.into_iter()
1128                    .filter_map(|s| Uuid::parse_str(&s).ok())
1129                    .collect()
1130            })
1131            .unwrap_or_default(),
1132        event_cursor: event_cursor_str.and_then(|s| Uuid::parse_str(&s).ok()),
1133        label: row.get(9)?,
1134        created_at: row.get(10)?,
1135        metadata: metadata_json
1136            .and_then(|s| serde_json::from_str(&s).ok())
1137            .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
1138    })
1139}
1140
1141fn row_to_delegation(row: &duckdb::Row<'_>) -> duckdb::Result<Delegation> {
1142    let id_str: String = row.get(0)?;
1143    let scope_type: String = row.get(4)?;
1144    let scope_value_json: Option<String> = row.get(5)?;
1145    let parent_id_str: Option<String> = row.get(8)?;
1146
1147    let scope = match scope_type.as_str() {
1148        "by_tag" => {
1149            let tags: Vec<String> = scope_value_json
1150                .and_then(|s| serde_json::from_str(&s).ok())
1151                .unwrap_or_default();
1152            DelegationScope::ByTag(tags)
1153        }
1154        "by_memory_id" => {
1155            let ids: Vec<String> = scope_value_json
1156                .and_then(|s| serde_json::from_str(&s).ok())
1157                .unwrap_or_default();
1158            let uuids = ids
1159                .into_iter()
1160                .filter_map(|s| Uuid::parse_str(&s).ok())
1161                .collect();
1162            DelegationScope::ByMemoryId(uuids)
1163        }
1164        _ => DelegationScope::AllMemories,
1165    };
1166
1167    let permission_str: String = row.get(3)?;
1168
1169    Ok(Delegation {
1170        id: Uuid::parse_str(&id_str).map_err(|e| {
1171            duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1172        })?,
1173        delegator_id: row.get(1)?,
1174        delegate_id: row.get(2)?,
1175        permission: permission_str.parse().map_err(|e: Error| {
1176            duckdb::Error::FromSqlConversionFailure(
1177                3,
1178                duckdb::types::Type::Text,
1179                e.to_string().into(),
1180            )
1181        })?,
1182        scope,
1183        max_depth: row.get::<_, i32>(6)? as u32,
1184        current_depth: row.get::<_, i32>(7)? as u32,
1185        parent_delegation_id: parent_id_str.and_then(|s| Uuid::parse_str(&s).ok()),
1186        created_at: row.get(9)?,
1187        expires_at: row.get(10)?,
1188        revoked_at: row.get(11)?,
1189    })
1190}
1191
1192fn row_to_relation(row: &duckdb::Row<'_>) -> duckdb::Result<Relation> {
1193    let id_str: String = row.get(0)?;
1194    let source_str: String = row.get(1)?;
1195    let target_str: String = row.get(2)?;
1196    let metadata_json: Option<String> = row.get(5)?;
1197
1198    Ok(Relation {
1199        id: Uuid::parse_str(&id_str).map_err(|e| {
1200            duckdb::Error::FromSqlConversionFailure(0, duckdb::types::Type::Text, Box::new(e))
1201        })?,
1202        source_id: Uuid::parse_str(&source_str).map_err(|e| {
1203            duckdb::Error::FromSqlConversionFailure(1, duckdb::types::Type::Text, Box::new(e))
1204        })?,
1205        target_id: Uuid::parse_str(&target_str).map_err(|e| {
1206            duckdb::Error::FromSqlConversionFailure(2, duckdb::types::Type::Text, Box::new(e))
1207        })?,
1208        relation_type: row.get(3)?,
1209        weight: row.get(4)?,
1210        metadata: metadata_json
1211            .and_then(|s| serde_json::from_str(&s).ok())
1212            .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
1213        created_at: row.get(6)?,
1214    })
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219    use super::*;
1220    use crate::hash::compute_content_hash;
1221    use crate::model::acl::PrincipalType;
1222    use crate::model::checkpoint::Checkpoint;
1223    use crate::model::event::{AgentEvent, EventType};
1224    use crate::model::memory::{ConsolidationState, MemoryType, Scope, SourceType};
1225
1226    fn make_record(agent_id: &str) -> MemoryRecord {
1227        let now = chrono::Utc::now().to_rfc3339();
1228        let content = "test memory content";
1229        MemoryRecord {
1230            id: Uuid::now_v7(),
1231            agent_id: agent_id.to_string(),
1232            content: content.to_string(),
1233            memory_type: MemoryType::Semantic,
1234            scope: Scope::Private,
1235            importance: 0.7,
1236            tags: vec!["test".to_string()],
1237            metadata: serde_json::json!({"key": "value"}),
1238            embedding: Some(vec![0.1, 0.2, 0.3]),
1239            content_hash: compute_content_hash(content, agent_id, &now),
1240            prev_hash: None,
1241            source_type: SourceType::Agent,
1242            source_id: None,
1243            consolidation_state: ConsolidationState::Raw,
1244            access_count: 0,
1245            org_id: None,
1246            thread_id: None,
1247            created_at: now.clone(),
1248            updated_at: now,
1249            last_accessed_at: None,
1250            expires_at: None,
1251            deleted_at: None,
1252            decay_rate: None,
1253            created_by: None,
1254            version: 1,
1255            prev_version_id: None,
1256            quarantined: false,
1257            quarantine_reason: None,
1258            decay_function: None,
1259        }
1260    }
1261
1262    #[tokio::test]
1263    async fn test_insert_and_get() {
1264        let storage = DuckDbStorage::open_in_memory().unwrap();
1265        let record = make_record("agent-1");
1266        storage.insert_memory(&record).await.unwrap();
1267
1268        let fetched = storage.get_memory(record.id).await.unwrap().unwrap();
1269        assert_eq!(fetched.id, record.id);
1270        assert_eq!(fetched.content, record.content);
1271        assert_eq!(fetched.agent_id, record.agent_id);
1272        assert_eq!(fetched.memory_type, record.memory_type);
1273        assert_eq!(fetched.tags, record.tags);
1274        assert_eq!(fetched.embedding, record.embedding);
1275    }
1276
1277    #[tokio::test]
1278    async fn test_get_nonexistent() {
1279        let storage = DuckDbStorage::open_in_memory().unwrap();
1280        let result = storage.get_memory(Uuid::now_v7()).await.unwrap();
1281        assert!(result.is_none());
1282    }
1283
1284    #[tokio::test]
1285    async fn test_soft_delete() {
1286        let storage = DuckDbStorage::open_in_memory().unwrap();
1287        let record = make_record("agent-1");
1288        storage.insert_memory(&record).await.unwrap();
1289
1290        storage.soft_delete_memory(record.id).await.unwrap();
1291
1292        // Should still exist in DB but with deleted_at set
1293        let fetched = storage.get_memory(record.id).await.unwrap().unwrap();
1294        assert!(fetched.deleted_at.is_some());
1295
1296        // Should not appear in list by default
1297        let filter = MemoryFilter::default();
1298        let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1299        assert!(list.is_empty());
1300
1301        // Should appear with include_deleted
1302        let filter_with_deleted = MemoryFilter {
1303            include_deleted: true,
1304            ..Default::default()
1305        };
1306        let list = storage
1307            .list_memories(&filter_with_deleted, 100, 0)
1308            .await
1309            .unwrap();
1310        assert_eq!(list.len(), 1);
1311    }
1312
1313    #[tokio::test]
1314    async fn test_hard_delete() {
1315        let storage = DuckDbStorage::open_in_memory().unwrap();
1316        let record = make_record("agent-1");
1317        storage.insert_memory(&record).await.unwrap();
1318
1319        storage.hard_delete_memory(record.id).await.unwrap();
1320
1321        let result = storage.get_memory(record.id).await.unwrap();
1322        assert!(result.is_none());
1323    }
1324
1325    #[tokio::test]
1326    async fn test_list_with_filters() {
1327        let storage = DuckDbStorage::open_in_memory().unwrap();
1328
1329        let mut r1 = make_record("agent-1");
1330        r1.memory_type = MemoryType::Episodic;
1331        storage.insert_memory(&r1).await.unwrap();
1332
1333        let mut r2 = make_record("agent-1");
1334        r2.memory_type = MemoryType::Semantic;
1335        storage.insert_memory(&r2).await.unwrap();
1336
1337        let mut r3 = make_record("agent-2");
1338        r3.memory_type = MemoryType::Semantic;
1339        storage.insert_memory(&r3).await.unwrap();
1340
1341        // Filter by agent
1342        let filter = MemoryFilter {
1343            agent_id: Some("agent-1".to_string()),
1344            ..Default::default()
1345        };
1346        let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1347        assert_eq!(list.len(), 2);
1348
1349        // Filter by type
1350        let filter = MemoryFilter {
1351            memory_type: Some(MemoryType::Semantic),
1352            ..Default::default()
1353        };
1354        let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1355        assert_eq!(list.len(), 2);
1356
1357        // Filter by agent + type
1358        let filter = MemoryFilter {
1359            agent_id: Some("agent-1".to_string()),
1360            memory_type: Some(MemoryType::Episodic),
1361            ..Default::default()
1362        };
1363        let list = storage.list_memories(&filter, 100, 0).await.unwrap();
1364        assert_eq!(list.len(), 1);
1365    }
1366
1367    #[tokio::test]
1368    async fn test_touch_memory() {
1369        let storage = DuckDbStorage::open_in_memory().unwrap();
1370        let record = make_record("agent-1");
1371        storage.insert_memory(&record).await.unwrap();
1372
1373        storage.touch_memory(record.id).await.unwrap();
1374        storage.touch_memory(record.id).await.unwrap();
1375
1376        let fetched = storage.get_memory(record.id).await.unwrap().unwrap();
1377        assert_eq!(fetched.access_count, 2);
1378        assert!(fetched.last_accessed_at.is_some());
1379    }
1380
1381    #[tokio::test]
1382    async fn test_acl_and_permission_check() {
1383        let storage = DuckDbStorage::open_in_memory().unwrap();
1384        let record = make_record("agent-1");
1385        storage.insert_memory(&record).await.unwrap();
1386
1387        // Owner always has permission
1388        assert!(
1389            storage
1390                .check_permission(record.id, "agent-1", Permission::Admin)
1391                .await
1392                .unwrap()
1393        );
1394
1395        // Non-owner has no permission by default
1396        assert!(
1397            !storage
1398                .check_permission(record.id, "agent-2", Permission::Read)
1399                .await
1400                .unwrap()
1401        );
1402
1403        // Grant read to agent-2
1404        let acl = Acl {
1405            id: Uuid::now_v7(),
1406            memory_id: record.id,
1407            principal_type: PrincipalType::Agent,
1408            principal_id: "agent-2".to_string(),
1409            permission: Permission::Read,
1410            granted_by: "agent-1".to_string(),
1411            created_at: chrono::Utc::now().to_rfc3339(),
1412            expires_at: None,
1413        };
1414        storage.insert_acl(&acl).await.unwrap();
1415
1416        // Now agent-2 can read
1417        assert!(
1418            storage
1419                .check_permission(record.id, "agent-2", Permission::Read)
1420                .await
1421                .unwrap()
1422        );
1423        // But not write
1424        assert!(
1425            !storage
1426                .check_permission(record.id, "agent-2", Permission::Write)
1427                .await
1428                .unwrap()
1429        );
1430    }
1431
1432    #[tokio::test]
1433    async fn test_event_insert_and_list() {
1434        let storage = DuckDbStorage::open_in_memory().unwrap();
1435        let now = chrono::Utc::now().to_rfc3339();
1436        let event = AgentEvent {
1437            id: Uuid::now_v7(),
1438            agent_id: "agent-1".to_string(),
1439            thread_id: Some("thread-1".to_string()),
1440            run_id: None,
1441            parent_event_id: None,
1442            event_type: EventType::MemoryWrite,
1443            payload: serde_json::json!({"memory_id": "abc"}),
1444            trace_id: None,
1445            span_id: None,
1446            model: None,
1447            tokens_input: None,
1448            tokens_output: None,
1449            latency_ms: None,
1450            cost_usd: None,
1451            timestamp: now.clone(),
1452            logical_clock: 1,
1453            content_hash: vec![1, 2, 3],
1454            prev_hash: None,
1455            embedding: None,
1456        };
1457
1458        storage.insert_event(&event).await.unwrap();
1459
1460        let events = storage.list_events("agent-1", 10, 0).await.unwrap();
1461        assert_eq!(events.len(), 1);
1462        assert_eq!(events[0].id, event.id);
1463        assert_eq!(events[0].event_type, EventType::MemoryWrite);
1464        assert_eq!(events[0].agent_id, "agent-1");
1465
1466        // Get single event
1467        let fetched = storage.get_event(event.id).await.unwrap().unwrap();
1468        assert_eq!(fetched.id, event.id);
1469        assert_eq!(fetched.content_hash, vec![1, 2, 3]);
1470    }
1471
1472    #[tokio::test]
1473    async fn test_events_by_thread() {
1474        let storage = DuckDbStorage::open_in_memory().unwrap();
1475        let now = chrono::Utc::now().to_rfc3339();
1476
1477        for i in 0..3 {
1478            let event = AgentEvent {
1479                id: Uuid::now_v7(),
1480                agent_id: "agent-1".to_string(),
1481                thread_id: Some("thread-A".to_string()),
1482                run_id: None,
1483                parent_event_id: None,
1484                event_type: EventType::MemoryWrite,
1485                payload: serde_json::json!({"i": i}),
1486                trace_id: None,
1487                span_id: None,
1488                model: None,
1489                tokens_input: None,
1490                tokens_output: None,
1491                latency_ms: None,
1492                cost_usd: None,
1493                timestamp: now.clone(),
1494                logical_clock: i,
1495                content_hash: vec![i as u8],
1496                prev_hash: None,
1497                embedding: None,
1498            };
1499            storage.insert_event(&event).await.unwrap();
1500        }
1501
1502        // Different thread
1503        let event = AgentEvent {
1504            id: Uuid::now_v7(),
1505            agent_id: "agent-1".to_string(),
1506            thread_id: Some("thread-B".to_string()),
1507            run_id: None,
1508            parent_event_id: None,
1509            event_type: EventType::MemoryRead,
1510            payload: serde_json::json!({}),
1511            trace_id: None,
1512            span_id: None,
1513            model: None,
1514            tokens_input: None,
1515            tokens_output: None,
1516            latency_ms: None,
1517            cost_usd: None,
1518            timestamp: now.clone(),
1519            logical_clock: 0,
1520            content_hash: vec![99],
1521            prev_hash: None,
1522            embedding: None,
1523        };
1524        storage.insert_event(&event).await.unwrap();
1525
1526        let thread_a = storage.get_events_by_thread("thread-A", 10).await.unwrap();
1527        assert_eq!(thread_a.len(), 3);
1528
1529        let thread_b = storage.get_events_by_thread("thread-B", 10).await.unwrap();
1530        assert_eq!(thread_b.len(), 1);
1531        assert_eq!(thread_b[0].event_type, EventType::MemoryRead);
1532    }
1533
1534    #[tokio::test]
1535    async fn test_checkpoint_insert_and_get() {
1536        let storage = DuckDbStorage::open_in_memory().unwrap();
1537        let mem_id = Uuid::now_v7();
1538        let cp = Checkpoint {
1539            id: Uuid::now_v7(),
1540            thread_id: "thread-1".to_string(),
1541            agent_id: "agent-1".to_string(),
1542            parent_id: None,
1543            branch_name: "main".to_string(),
1544            state_snapshot: serde_json::json!({"step": 1}),
1545            state_diff: None,
1546            memory_refs: vec![mem_id],
1547            event_cursor: None,
1548            label: Some("initial".to_string()),
1549            created_at: chrono::Utc::now().to_rfc3339(),
1550            metadata: serde_json::json!({}),
1551        };
1552
1553        storage.insert_checkpoint(&cp).await.unwrap();
1554
1555        let fetched = storage.get_checkpoint(cp.id).await.unwrap().unwrap();
1556        assert_eq!(fetched.id, cp.id);
1557        assert_eq!(fetched.thread_id, "thread-1");
1558        assert_eq!(fetched.branch_name, "main");
1559        assert_eq!(fetched.memory_refs, vec![mem_id]);
1560        assert_eq!(fetched.label, Some("initial".to_string()));
1561    }
1562
1563    #[tokio::test]
1564    async fn test_checkpoint_list_and_latest() {
1565        let storage = DuckDbStorage::open_in_memory().unwrap();
1566
1567        let cp1 = Checkpoint {
1568            id: Uuid::now_v7(),
1569            thread_id: "thread-1".to_string(),
1570            agent_id: "agent-1".to_string(),
1571            parent_id: None,
1572            branch_name: "main".to_string(),
1573            state_snapshot: serde_json::json!({"step": 1}),
1574            state_diff: None,
1575            memory_refs: vec![],
1576            event_cursor: None,
1577            label: Some("first".to_string()),
1578            created_at: "2025-01-01T00:00:00Z".to_string(),
1579            metadata: serde_json::json!({}),
1580        };
1581        storage.insert_checkpoint(&cp1).await.unwrap();
1582
1583        let cp2 = Checkpoint {
1584            id: Uuid::now_v7(),
1585            thread_id: "thread-1".to_string(),
1586            agent_id: "agent-1".to_string(),
1587            parent_id: Some(cp1.id),
1588            branch_name: "main".to_string(),
1589            state_snapshot: serde_json::json!({"step": 2}),
1590            state_diff: Some(serde_json::json!({"step": [1, 2]})),
1591            memory_refs: vec![],
1592            event_cursor: None,
1593            label: Some("second".to_string()),
1594            created_at: "2025-01-02T00:00:00Z".to_string(),
1595            metadata: serde_json::json!({}),
1596        };
1597        storage.insert_checkpoint(&cp2).await.unwrap();
1598
1599        let cp3 = Checkpoint {
1600            id: Uuid::now_v7(),
1601            thread_id: "thread-1".to_string(),
1602            agent_id: "agent-1".to_string(),
1603            parent_id: Some(cp1.id),
1604            branch_name: "experiment".to_string(),
1605            state_snapshot: serde_json::json!({"step": "alt"}),
1606            state_diff: None,
1607            memory_refs: vec![],
1608            event_cursor: None,
1609            label: None,
1610            created_at: "2025-01-03T00:00:00Z".to_string(),
1611            metadata: serde_json::json!({}),
1612        };
1613        storage.insert_checkpoint(&cp3).await.unwrap();
1614
1615        // List all for thread
1616        let all = storage
1617            .list_checkpoints("thread-1", None, 10)
1618            .await
1619            .unwrap();
1620        assert_eq!(all.len(), 3);
1621
1622        // List by branch
1623        let main_cps = storage
1624            .list_checkpoints("thread-1", Some("main"), 10)
1625            .await
1626            .unwrap();
1627        assert_eq!(main_cps.len(), 2);
1628
1629        let exp_cps = storage
1630            .list_checkpoints("thread-1", Some("experiment"), 10)
1631            .await
1632            .unwrap();
1633        assert_eq!(exp_cps.len(), 1);
1634
1635        // Latest on main
1636        let latest = storage
1637            .get_latest_checkpoint("thread-1", "main")
1638            .await
1639            .unwrap()
1640            .unwrap();
1641        assert_eq!(latest.id, cp2.id);
1642
1643        // Latest on experiment
1644        let latest_exp = storage
1645            .get_latest_checkpoint("thread-1", "experiment")
1646            .await
1647            .unwrap()
1648            .unwrap();
1649        assert_eq!(latest_exp.id, cp3.id);
1650
1651        // No checkpoints for nonexistent branch
1652        let none = storage
1653            .get_latest_checkpoint("thread-1", "nonexistent")
1654            .await
1655            .unwrap();
1656        assert!(none.is_none());
1657    }
1658}