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