1use mnemo_core::error::{Error, Result};
2use mnemo_core::model::acl::{Acl, Permission};
3use mnemo_core::model::agent_profile::AgentProfile;
4use mnemo_core::model::checkpoint::Checkpoint;
5use mnemo_core::model::delegation::{Delegation, DelegationScope};
6use mnemo_core::model::embedding_baseline::EmbeddingBaseline;
7use mnemo_core::model::event::AgentEvent;
8use mnemo_core::model::memory::MemoryRecord;
9use mnemo_core::model::relation::Relation;
10use mnemo_core::storage::{MemoryFilter, StorageBackend};
11use pgvector::Vector;
12use sqlx::Row;
13use uuid::Uuid;
14
15pub struct PgStorage {
22 pool: sqlx::PgPool,
23 dimensions: usize,
24}
25
26impl PgStorage {
27 pub async fn connect(url: &str, dimensions: usize) -> Result<Self> {
32 let pool = sqlx::PgPool::connect(url)
33 .await
34 .map_err(|e| Error::Storage(e.to_string()))?;
35 let storage = Self { pool, dimensions };
36 crate::migrations::run_migrations(&storage.pool, dimensions).await?;
37 Ok(storage)
38 }
39
40 pub async fn from_pool(pool: sqlx::PgPool, dimensions: usize) -> Result<Self> {
42 crate::migrations::run_migrations(&pool, dimensions).await?;
43 Ok(Self { pool, dimensions })
44 }
45
46 pub fn pool(&self) -> sqlx::PgPool {
50 self.pool.clone()
51 }
52
53 pub fn dimensions(&self) -> usize {
55 self.dimensions
56 }
57}
58
59fn map_sqlx(e: sqlx::Error) -> Error {
64 Error::Storage(e.to_string())
65}
66
67fn serialize_embedding(embedding: &Option<Vec<f32>>) -> Option<Vec<u8>> {
68 embedding
69 .as_ref()
70 .map(|v| v.iter().flat_map(|f| f.to_le_bytes()).collect())
71}
72
73fn deserialize_embedding(blob: Option<Vec<u8>>) -> Option<Vec<f32>> {
74 blob.map(|bytes| {
75 bytes
76 .chunks_exact(4)
77 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
78 .collect()
79 })
80}
81
82fn row_to_memory(row: &sqlx::postgres::PgRow) -> std::result::Result<MemoryRecord, sqlx::Error> {
83 let tags: Vec<String> = row.try_get::<Vec<String>, _>("tags").unwrap_or_default();
84 let metadata: serde_json::Value = row
85 .try_get("metadata")
86 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
87
88 let embedding: Option<Vec<f32>> = {
91 let raw: Option<String> = row.try_get("embedding_text").ok().flatten();
92 raw.and_then(|s| {
93 let trimmed = s.trim_start_matches('[').trim_end_matches(']');
95 if trimmed.is_empty() {
96 None
97 } else {
98 Some(
99 trimmed
100 .split(',')
101 .filter_map(|v| v.trim().parse::<f32>().ok())
102 .collect(),
103 )
104 }
105 })
106 };
107
108 Ok(MemoryRecord {
109 id: row.get("id"),
110 agent_id: row.get("agent_id"),
111 content: row.get("content"),
112 memory_type: row
113 .get::<String, _>("memory_type")
114 .parse()
115 .unwrap_or(mnemo_core::model::memory::MemoryType::Semantic),
116 scope: row
117 .get::<String, _>("scope")
118 .parse()
119 .unwrap_or(mnemo_core::model::memory::Scope::Private),
120 importance: row.get("importance"),
121 tags,
122 metadata,
123 embedding,
124 content_hash: row.get("content_hash"),
125 prev_hash: row.get("prev_hash"),
126 source_type: row
127 .get::<String, _>("source_type")
128 .parse()
129 .unwrap_or(mnemo_core::model::memory::SourceType::Agent),
130 source_id: row.get("source_id"),
131 consolidation_state: row
132 .get::<String, _>("consolidation_state")
133 .parse()
134 .unwrap_or(mnemo_core::model::memory::ConsolidationState::Raw),
135 access_count: row.get::<i64, _>("access_count") as u64,
136 org_id: row.get("org_id"),
137 thread_id: row.get("thread_id"),
138 created_at: row.get("created_at"),
139 updated_at: row.get("updated_at"),
140 last_accessed_at: row.get("last_accessed_at"),
141 expires_at: row.get("expires_at"),
142 deleted_at: row.get("deleted_at"),
143 decay_rate: row.get("decay_rate"),
144 created_by: row.get("created_by"),
145 version: row.get::<i32, _>("version") as u32,
146 prev_version_id: row.get("prev_version_id"),
147 quarantined: row.get("quarantined"),
148 quarantine_reason: row.get("quarantine_reason"),
149 decay_function: row.get("decay_function"),
150 })
151}
152
153const MEMORY_COLUMNS: &str = r#"
163 id, agent_id, content, memory_type, scope, importance,
164 tags, metadata, embedding::text AS embedding_text,
165 content_hash, prev_hash, source_type, source_id,
166 consolidation_state, access_count, org_id, thread_id,
167 created_at, updated_at, last_accessed_at, expires_at,
168 deleted_at, decay_rate, created_by, version, prev_version_id,
169 quarantined, quarantine_reason, decay_function
170"#;
171
172fn row_to_event(row: &sqlx::postgres::PgRow) -> std::result::Result<AgentEvent, sqlx::Error> {
173 let payload: serde_json::Value = row.try_get("payload").unwrap_or(serde_json::Value::Null);
174 let embedding_blob: Option<Vec<u8>> = row.try_get("embedding").unwrap_or(None);
175
176 Ok(AgentEvent {
177 id: row.get("id"),
178 agent_id: row.get("agent_id"),
179 thread_id: row.get("thread_id"),
180 run_id: row.get("run_id"),
181 parent_event_id: row.get("parent_event_id"),
182 event_type: row
183 .get::<String, _>("event_type")
184 .parse()
185 .unwrap_or(mnemo_core::model::event::EventType::Error),
186 payload,
187 trace_id: row.get("trace_id"),
188 span_id: row.get("span_id"),
189 model: row.get("model"),
190 tokens_input: row.get("tokens_input"),
191 tokens_output: row.get("tokens_output"),
192 latency_ms: row.get("latency_ms"),
193 cost_usd: row.get("cost_usd"),
194 timestamp: row.get("timestamp"),
195 logical_clock: row.get("logical_clock"),
196 content_hash: row.get("content_hash"),
197 prev_hash: row.get("prev_hash"),
198 embedding: deserialize_embedding(embedding_blob),
199 })
200}
201
202fn row_to_relation(row: &sqlx::postgres::PgRow) -> std::result::Result<Relation, sqlx::Error> {
203 let metadata: serde_json::Value = row
204 .try_get("metadata")
205 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
206
207 Ok(Relation {
208 id: row.get("id"),
209 source_id: row.get("source_id"),
210 target_id: row.get("target_id"),
211 relation_type: row.get("relation_type"),
212 weight: row.get("weight"),
213 metadata,
214 created_at: row.get("created_at"),
215 })
216}
217
218fn row_to_checkpoint(row: &sqlx::postgres::PgRow) -> std::result::Result<Checkpoint, sqlx::Error> {
219 let state_snapshot: serde_json::Value = row
220 .try_get("state_snapshot")
221 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
222 let state_diff: Option<serde_json::Value> = row.try_get("state_diff").unwrap_or(None);
223
224 let memory_refs_raw: Vec<String> = row.try_get("memory_refs").unwrap_or_default();
226 let memory_refs: Vec<Uuid> = memory_refs_raw
227 .iter()
228 .filter_map(|s| Uuid::parse_str(s).ok())
229 .collect();
230
231 let metadata: serde_json::Value = row
232 .try_get("metadata")
233 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
234
235 Ok(Checkpoint {
236 id: row.get("id"),
237 thread_id: row.get("thread_id"),
238 agent_id: row.get("agent_id"),
239 parent_id: row.get("parent_id"),
240 branch_name: row.get("branch_name"),
241 state_snapshot,
242 state_diff,
243 memory_refs,
244 event_cursor: row.get("event_cursor"),
245 label: row.get("label"),
246 created_at: row.get("created_at"),
247 metadata,
248 })
249}
250
251fn row_to_delegation(row: &sqlx::postgres::PgRow) -> std::result::Result<Delegation, sqlx::Error> {
252 let scope_type: String = row.get("scope_type");
253 let scope_value: Option<serde_json::Value> = row.try_get("scope_value").unwrap_or(None);
254
255 let scope = match scope_type.as_str() {
256 "by_tag" => {
257 let tags: Vec<String> = scope_value
258 .and_then(|v| serde_json::from_value(v).ok())
259 .unwrap_or_default();
260 DelegationScope::ByTag(tags)
261 }
262 "by_memory_id" => {
263 let id_strs: Vec<String> = scope_value
264 .and_then(|v| serde_json::from_value(v).ok())
265 .unwrap_or_default();
266 let uuids = id_strs
267 .into_iter()
268 .filter_map(|s| Uuid::parse_str(&s).ok())
269 .collect();
270 DelegationScope::ByMemoryId(uuids)
271 }
272 _ => DelegationScope::AllMemories,
273 };
274
275 Ok(Delegation {
276 id: row.get("id"),
277 delegator_id: row.get("delegator_id"),
278 delegate_id: row.get("delegate_id"),
279 permission: row
280 .get::<String, _>("permission")
281 .parse()
282 .unwrap_or(Permission::Read),
283 scope,
284 max_depth: row.get::<i32, _>("max_depth") as u32,
285 current_depth: row.get::<i32, _>("current_depth") as u32,
286 parent_delegation_id: row.get("parent_delegation_id"),
287 created_at: row.get("created_at"),
288 expires_at: row.get("expires_at"),
289 revoked_at: row.get("revoked_at"),
290 })
291}
292
293#[async_trait::async_trait]
298impl StorageBackend for PgStorage {
299 fn backend_name(&self) -> &'static str {
300 "postgres"
301 }
302
303 async fn insert_memory(&self, record: &MemoryRecord) -> Result<()> {
308 let embedding_param: Option<Vector> =
309 record.embedding.as_ref().map(|v| Vector::from(v.clone()));
310
311 let tags_slice: &[String] = &record.tags;
312
313 sqlx::query(
314 r#"
315INSERT INTO memories (
316 id, agent_id, content, memory_type, scope, importance,
317 tags, metadata, embedding,
318 content_hash, prev_hash, source_type, source_id,
319 consolidation_state, access_count, org_id, thread_id,
320 created_at, updated_at, last_accessed_at, expires_at,
321 deleted_at, decay_rate, created_by, version, prev_version_id,
322 quarantined, quarantine_reason, decay_function
323) VALUES (
324 $1, $2, $3, $4, $5, $6,
325 $7, $8, $9,
326 $10, $11, $12, $13,
327 $14, $15, $16, $17,
328 $18, $19, $20, $21,
329 $22, $23, $24, $25, $26,
330 $27, $28, $29
331)
332"#,
333 )
334 .bind(record.id)
335 .bind(&record.agent_id)
336 .bind(&record.content)
337 .bind(record.memory_type.to_string())
338 .bind(record.scope.to_string())
339 .bind(record.importance)
340 .bind(tags_slice)
341 .bind(&record.metadata)
342 .bind(&embedding_param)
343 .bind(&record.content_hash)
344 .bind(&record.prev_hash)
345 .bind(record.source_type.to_string())
346 .bind(&record.source_id)
347 .bind(record.consolidation_state.to_string())
348 .bind(record.access_count as i64)
349 .bind(&record.org_id)
350 .bind(&record.thread_id)
351 .bind(&record.created_at)
352 .bind(&record.updated_at)
353 .bind(&record.last_accessed_at)
354 .bind(&record.expires_at)
355 .bind(&record.deleted_at)
356 .bind(record.decay_rate)
357 .bind(&record.created_by)
358 .bind(record.version as i32)
359 .bind(record.prev_version_id)
360 .bind(record.quarantined)
361 .bind(&record.quarantine_reason)
362 .bind(&record.decay_function)
363 .execute(&self.pool)
364 .await
365 .map_err(map_sqlx)?;
366
367 Ok(())
368 }
369
370 async fn get_memory(&self, id: Uuid) -> Result<Option<MemoryRecord>> {
371 let sql = format!("SELECT {MEMORY_COLUMNS} FROM memories WHERE id = $1");
372 let row = sqlx::query(sqlx::AssertSqlSafe(sql.as_str()))
373 .bind(id)
374 .fetch_optional(&self.pool)
375 .await
376 .map_err(map_sqlx)?;
377
378 match row {
379 Some(r) => Ok(Some(row_to_memory(&r).map_err(map_sqlx)?)),
380 None => Ok(None),
381 }
382 }
383
384 async fn update_memory(&self, record: &MemoryRecord) -> Result<()> {
385 let embedding_param: Option<Vector> =
386 record.embedding.as_ref().map(|v| Vector::from(v.clone()));
387
388 let tags_slice: &[String] = &record.tags;
389
390 let result = sqlx::query(
391 r#"
392UPDATE memories SET
393 agent_id = $1, content = $2, memory_type = $3, scope = $4,
394 importance = $5, tags = $6, metadata = $7,
395 embedding = $8,
396 content_hash = $9, prev_hash = $10, source_type = $11,
397 source_id = $12, consolidation_state = $13, access_count = $14,
398 org_id = $15, thread_id = $16, updated_at = $17,
399 last_accessed_at = $18, expires_at = $19, deleted_at = $20,
400 decay_rate = $21, created_by = $22, version = $23,
401 prev_version_id = $24, quarantined = $25, quarantine_reason = $26,
402 decay_function = $27
403WHERE id = $28
404"#,
405 )
406 .bind(&record.agent_id)
407 .bind(&record.content)
408 .bind(record.memory_type.to_string())
409 .bind(record.scope.to_string())
410 .bind(record.importance)
411 .bind(tags_slice)
412 .bind(&record.metadata)
413 .bind(&embedding_param)
414 .bind(&record.content_hash)
415 .bind(&record.prev_hash)
416 .bind(record.source_type.to_string())
417 .bind(&record.source_id)
418 .bind(record.consolidation_state.to_string())
419 .bind(record.access_count as i64)
420 .bind(&record.org_id)
421 .bind(&record.thread_id)
422 .bind(&record.updated_at)
423 .bind(&record.last_accessed_at)
424 .bind(&record.expires_at)
425 .bind(&record.deleted_at)
426 .bind(record.decay_rate)
427 .bind(&record.created_by)
428 .bind(record.version as i32)
429 .bind(record.prev_version_id)
430 .bind(record.quarantined)
431 .bind(&record.quarantine_reason)
432 .bind(&record.decay_function)
433 .bind(record.id)
434 .execute(&self.pool)
435 .await
436 .map_err(map_sqlx)?;
437
438 if result.rows_affected() == 0 {
439 return Err(Error::NotFound(format!("memory {} not found", record.id)));
440 }
441 Ok(())
442 }
443
444 async fn soft_delete_memory(&self, id: Uuid) -> Result<()> {
445 let now = chrono::Utc::now().to_rfc3339();
446 let result = sqlx::query(
447 "UPDATE memories SET deleted_at = $1, updated_at = $2 WHERE id = $3 AND deleted_at IS NULL",
448 )
449 .bind(&now)
450 .bind(&now)
451 .bind(id)
452 .execute(&self.pool)
453 .await
454 .map_err(map_sqlx)?;
455
456 if result.rows_affected() == 0 {
457 return Err(Error::NotFound(format!(
458 "memory {id} not found or already deleted"
459 )));
460 }
461 Ok(())
462 }
463
464 async fn hard_delete_memory(&self, id: Uuid) -> Result<()> {
465 let result = sqlx::query("DELETE FROM memories WHERE id = $1")
466 .bind(id)
467 .execute(&self.pool)
468 .await
469 .map_err(map_sqlx)?;
470
471 if result.rows_affected() == 0 {
472 return Err(Error::NotFound(format!("memory {id} not found")));
473 }
474
475 sqlx::query("DELETE FROM acls WHERE memory_id = $1")
477 .bind(id)
478 .execute(&self.pool)
479 .await
480 .map_err(map_sqlx)?;
481
482 Ok(())
483 }
484
485 async fn list_memories(
486 &self,
487 filter: &MemoryFilter,
488 limit: usize,
489 offset: usize,
490 ) -> Result<Vec<MemoryRecord>> {
491 let mut conditions: Vec<String> = Vec::new();
492 let mut param_idx: usize = 0;
495
496 if !filter.include_deleted {
506 conditions.push("deleted_at IS NULL".to_string());
507 }
508
509 #[derive(Debug)]
511 enum Param {
512 Str(String),
513 F32(f32),
514 }
515 let mut params: Vec<Param> = Vec::new();
516
517 if let Some(ref agent_id) = filter.agent_id {
518 param_idx += 1;
519 conditions.push(format!("agent_id = ${param_idx}"));
520 params.push(Param::Str(agent_id.clone()));
521 }
522 if let Some(memory_type) = filter.memory_type {
523 param_idx += 1;
524 conditions.push(format!("memory_type = ${param_idx}"));
525 params.push(Param::Str(memory_type.to_string()));
526 }
527 if let Some(scope) = filter.scope {
528 param_idx += 1;
529 conditions.push(format!("scope = ${param_idx}"));
530 params.push(Param::Str(scope.to_string()));
531 }
532 if let Some(min_importance) = filter.min_importance {
533 param_idx += 1;
534 conditions.push(format!("importance >= ${param_idx}"));
535 params.push(Param::F32(min_importance));
536 }
537 if let Some(ref org_id) = filter.org_id {
538 param_idx += 1;
539 conditions.push(format!("org_id = ${param_idx}"));
540 params.push(Param::Str(org_id.clone()));
541 }
542 if let Some(ref thread_id) = filter.thread_id {
543 param_idx += 1;
544 conditions.push(format!("thread_id = ${param_idx}"));
545 params.push(Param::Str(thread_id.clone()));
546 }
547
548 let where_clause = if conditions.is_empty() {
549 String::new()
550 } else {
551 format!("WHERE {}", conditions.join(" AND "))
552 };
553
554 let sql = format!(
555 "SELECT {MEMORY_COLUMNS} FROM memories {where_clause} ORDER BY created_at DESC LIMIT {limit} OFFSET {offset}"
556 );
557
558 let mut query = sqlx::query(sqlx::AssertSqlSafe(sql.as_str()));
559 for p in ¶ms {
560 match p {
561 Param::Str(s) => query = query.bind(s),
562 Param::F32(f) => query = query.bind(*f),
563 }
564 }
565
566 let rows = query.fetch_all(&self.pool).await.map_err(map_sqlx)?;
567 let mut results = Vec::with_capacity(rows.len());
568 for r in &rows {
569 results.push(row_to_memory(r).map_err(map_sqlx)?);
570 }
571 Ok(results)
572 }
573
574 async fn touch_memory(&self, id: Uuid) -> Result<()> {
575 let now = chrono::Utc::now().to_rfc3339();
576 sqlx::query(
577 "UPDATE memories SET access_count = access_count + 1, last_accessed_at = $1 WHERE id = $2",
578 )
579 .bind(&now)
580 .bind(id)
581 .execute(&self.pool)
582 .await
583 .map_err(map_sqlx)?;
584 Ok(())
585 }
586
587 async fn insert_acl(&self, acl: &Acl) -> Result<()> {
592 sqlx::query(
593 r#"
594INSERT INTO acls (id, memory_id, principal_type, principal_id, permission, granted_by, created_at, expires_at)
595VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
596"#,
597 )
598 .bind(acl.id)
599 .bind(acl.memory_id)
600 .bind(acl.principal_type.to_string())
601 .bind(&acl.principal_id)
602 .bind(acl.permission.to_string())
603 .bind(&acl.granted_by)
604 .bind(&acl.created_at)
605 .bind(&acl.expires_at)
606 .execute(&self.pool)
607 .await
608 .map_err(map_sqlx)?;
609 Ok(())
610 }
611
612 async fn check_permission(
613 &self,
614 memory_id: Uuid,
615 principal_id: &str,
616 required: Permission,
617 ) -> Result<bool> {
618 let owner_row = sqlx::query("SELECT agent_id FROM memories WHERE id = $1")
620 .bind(memory_id)
621 .fetch_optional(&self.pool)
622 .await
623 .map_err(map_sqlx)?;
624
625 match owner_row {
626 None => return Err(Error::NotFound(format!("memory {memory_id} not found"))),
627 Some(row) => {
628 let owner: String = row.get("agent_id");
629 if owner == principal_id {
630 return Ok(true);
631 }
632 }
633 }
634
635 let now = chrono::Utc::now().to_rfc3339();
637 let acl_rows = sqlx::query(
638 "SELECT permission FROM acls WHERE memory_id = $1 AND principal_id = $2 AND (expires_at IS NULL OR expires_at > $3)",
639 )
640 .bind(memory_id)
641 .bind(principal_id)
642 .bind(&now)
643 .fetch_all(&self.pool)
644 .await
645 .map_err(map_sqlx)?;
646
647 for row in &acl_rows {
648 let perm_str: String = row.get("permission");
649 if let Ok(perm) = perm_str.parse::<Permission>()
650 && perm.satisfies(required)
651 {
652 return Ok(true);
653 }
654 }
655
656 let public_rows = sqlx::query(
658 "SELECT permission FROM acls WHERE memory_id = $1 AND principal_type = 'public' AND (expires_at IS NULL OR expires_at > $2)",
659 )
660 .bind(memory_id)
661 .bind(&now)
662 .fetch_all(&self.pool)
663 .await
664 .map_err(map_sqlx)?;
665
666 for row in &public_rows {
667 let perm_str: String = row.get("permission");
668 if let Ok(perm) = perm_str.parse::<Permission>()
669 && perm.satisfies(required)
670 {
671 return Ok(true);
672 }
673 }
674
675 if self
677 .check_delegation(principal_id, memory_id, required)
678 .await?
679 {
680 return Ok(true);
681 }
682
683 Ok(false)
684 }
685
686 async fn insert_relation(&self, relation: &Relation) -> Result<()> {
691 sqlx::query(
692 r#"
693INSERT INTO relations (id, source_id, target_id, relation_type, weight, metadata, created_at)
694VALUES ($1, $2, $3, $4, $5, $6, $7)
695"#,
696 )
697 .bind(relation.id)
698 .bind(relation.source_id)
699 .bind(relation.target_id)
700 .bind(&relation.relation_type)
701 .bind(relation.weight)
702 .bind(&relation.metadata)
703 .bind(&relation.created_at)
704 .execute(&self.pool)
705 .await
706 .map_err(map_sqlx)?;
707 Ok(())
708 }
709
710 async fn get_relations_from(&self, source_id: Uuid) -> Result<Vec<Relation>> {
711 let rows = sqlx::query(
712 "SELECT id, source_id, target_id, relation_type, weight, metadata, created_at FROM relations WHERE source_id = $1",
713 )
714 .bind(source_id)
715 .fetch_all(&self.pool)
716 .await
717 .map_err(map_sqlx)?;
718
719 let mut results = Vec::with_capacity(rows.len());
720 for r in &rows {
721 results.push(row_to_relation(r).map_err(map_sqlx)?);
722 }
723 Ok(results)
724 }
725
726 async fn get_relations_to(&self, target_id: Uuid) -> Result<Vec<Relation>> {
727 let rows = sqlx::query(
728 "SELECT id, source_id, target_id, relation_type, weight, metadata, created_at FROM relations WHERE target_id = $1",
729 )
730 .bind(target_id)
731 .fetch_all(&self.pool)
732 .await
733 .map_err(map_sqlx)?;
734
735 let mut results = Vec::with_capacity(rows.len());
736 for r in &rows {
737 results.push(row_to_relation(r).map_err(map_sqlx)?);
738 }
739 Ok(results)
740 }
741
742 async fn delete_relation(&self, id: Uuid) -> Result<()> {
743 let result = sqlx::query("DELETE FROM relations WHERE id = $1")
744 .bind(id)
745 .execute(&self.pool)
746 .await
747 .map_err(map_sqlx)?;
748
749 if result.rows_affected() == 0 {
750 return Err(Error::NotFound(format!("relation {id} not found")));
751 }
752 Ok(())
753 }
754
755 async fn get_latest_memory_hash(
760 &self,
761 agent_id: &str,
762 thread_id: Option<&str>,
763 ) -> Result<Option<Vec<u8>>> {
764 let row = if let Some(tid) = thread_id {
765 sqlx::query(
766 "SELECT content_hash FROM memories WHERE agent_id = $1 AND thread_id = $2 AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1",
767 )
768 .bind(agent_id)
769 .bind(tid)
770 .fetch_optional(&self.pool)
771 .await
772 .map_err(map_sqlx)?
773 } else {
774 sqlx::query(
775 "SELECT content_hash FROM memories WHERE agent_id = $1 AND thread_id IS NULL AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1",
776 )
777 .bind(agent_id)
778 .fetch_optional(&self.pool)
779 .await
780 .map_err(map_sqlx)?
781 };
782
783 Ok(row.map(|r| r.get::<Vec<u8>, _>("content_hash")))
784 }
785
786 async fn get_latest_event_hash(
787 &self,
788 agent_id: &str,
789 thread_id: Option<&str>,
790 ) -> Result<Option<Vec<u8>>> {
791 let row = if let Some(tid) = thread_id {
792 sqlx::query(
793 "SELECT content_hash FROM agent_events WHERE agent_id = $1 AND thread_id = $2 ORDER BY timestamp DESC LIMIT 1",
794 )
795 .bind(agent_id)
796 .bind(tid)
797 .fetch_optional(&self.pool)
798 .await
799 .map_err(map_sqlx)?
800 } else {
801 sqlx::query(
802 "SELECT content_hash FROM agent_events WHERE agent_id = $1 ORDER BY timestamp DESC LIMIT 1",
803 )
804 .bind(agent_id)
805 .fetch_optional(&self.pool)
806 .await
807 .map_err(map_sqlx)?
808 };
809 Ok(row.map(|r| r.get::<Vec<u8>, _>("content_hash")))
810 }
811
812 async fn get_sync_watermark(&self, key: &str) -> Result<Option<String>> {
813 let row = sqlx::query("SELECT value FROM sync_metadata WHERE key = $1")
814 .bind(key)
815 .fetch_optional(&self.pool)
816 .await
817 .map_err(map_sqlx)?;
818 Ok(row.map(|r| r.get::<String, _>("value")))
819 }
820
821 async fn set_sync_watermark(&self, key: &str, value: &str) -> Result<()> {
822 let now = chrono::Utc::now().to_rfc3339();
823 sqlx::query(
824 "INSERT INTO sync_metadata (key, value, updated_at) VALUES ($1, $2, $3) ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = $3",
825 )
826 .bind(key)
827 .bind(value)
828 .bind(now)
829 .execute(&self.pool)
830 .await
831 .map_err(map_sqlx)?;
832 Ok(())
833 }
834
835 async fn list_accessible_memory_ids(&self, agent_id: &str, limit: usize) -> Result<Vec<Uuid>> {
840 let now = chrono::Utc::now().to_rfc3339();
841 let rows = sqlx::query(
842 r#"
843SELECT id FROM memories
844WHERE (
845 agent_id = $1
846 OR scope = 'public'
847 OR id IN (
848 SELECT memory_id FROM acls
849 WHERE principal_id = $2 AND (expires_at IS NULL OR expires_at > $3)
850 )
851)
852AND deleted_at IS NULL
853LIMIT $4
854"#,
855 )
856 .bind(agent_id)
857 .bind(agent_id)
858 .bind(&now)
859 .bind(limit as i64)
860 .fetch_all(&self.pool)
861 .await
862 .map_err(map_sqlx)?;
863
864 let ids: Vec<Uuid> = rows.iter().map(|r| r.get("id")).collect();
865 Ok(ids)
866 }
867
868 async fn insert_event(&self, event: &AgentEvent) -> Result<()> {
873 let payload_json = &event.payload;
874 let embedding_blob = serialize_embedding(&event.embedding);
875
876 sqlx::query(
877 r#"
878INSERT INTO agent_events (
879 id, agent_id, thread_id, run_id, parent_event_id, event_type,
880 payload, trace_id, span_id, model, tokens_input, tokens_output,
881 latency_ms, cost_usd, "timestamp", logical_clock, content_hash,
882 prev_hash, embedding
883) VALUES (
884 $1, $2, $3, $4, $5, $6,
885 $7, $8, $9, $10, $11, $12,
886 $13, $14, $15, $16, $17,
887 $18, $19
888)
889"#,
890 )
891 .bind(event.id)
892 .bind(&event.agent_id)
893 .bind(&event.thread_id)
894 .bind(&event.run_id)
895 .bind(event.parent_event_id)
896 .bind(event.event_type.to_string())
897 .bind(payload_json)
898 .bind(&event.trace_id)
899 .bind(&event.span_id)
900 .bind(&event.model)
901 .bind(event.tokens_input)
902 .bind(event.tokens_output)
903 .bind(event.latency_ms)
904 .bind(event.cost_usd)
905 .bind(&event.timestamp)
906 .bind(event.logical_clock)
907 .bind(&event.content_hash)
908 .bind(&event.prev_hash)
909 .bind(&embedding_blob)
910 .execute(&self.pool)
911 .await
912 .map_err(map_sqlx)?;
913 Ok(())
914 }
915
916 async fn list_events(
917 &self,
918 agent_id: &str,
919 limit: usize,
920 offset: usize,
921 ) -> Result<Vec<AgentEvent>> {
922 let rows = sqlx::query(
923 r#"
924SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type,
925 payload, trace_id, span_id, model, tokens_input, tokens_output,
926 latency_ms, cost_usd, "timestamp", logical_clock, content_hash,
927 prev_hash, embedding
928FROM agent_events
929WHERE agent_id = $1
930ORDER BY "timestamp" DESC
931LIMIT $2 OFFSET $3
932"#,
933 )
934 .bind(agent_id)
935 .bind(limit as i64)
936 .bind(offset as i64)
937 .fetch_all(&self.pool)
938 .await
939 .map_err(map_sqlx)?;
940
941 let mut results = Vec::with_capacity(rows.len());
942 for r in &rows {
943 results.push(row_to_event(r).map_err(map_sqlx)?);
944 }
945 Ok(results)
946 }
947
948 async fn get_events_by_thread(&self, thread_id: &str, limit: usize) -> Result<Vec<AgentEvent>> {
949 let rows = sqlx::query(
950 r#"
951SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type,
952 payload, trace_id, span_id, model, tokens_input, tokens_output,
953 latency_ms, cost_usd, "timestamp", logical_clock, content_hash,
954 prev_hash, embedding
955FROM agent_events
956WHERE thread_id = $1
957ORDER BY "timestamp" ASC
958LIMIT $2
959"#,
960 )
961 .bind(thread_id)
962 .bind(limit as i64)
963 .fetch_all(&self.pool)
964 .await
965 .map_err(map_sqlx)?;
966
967 let mut results = Vec::with_capacity(rows.len());
968 for r in &rows {
969 results.push(row_to_event(r).map_err(map_sqlx)?);
970 }
971 Ok(results)
972 }
973
974 async fn get_event(&self, id: Uuid) -> Result<Option<AgentEvent>> {
975 let row = sqlx::query(
976 r#"
977SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type,
978 payload, trace_id, span_id, model, tokens_input, tokens_output,
979 latency_ms, cost_usd, "timestamp", logical_clock, content_hash,
980 prev_hash, embedding
981FROM agent_events
982WHERE id = $1
983"#,
984 )
985 .bind(id)
986 .fetch_optional(&self.pool)
987 .await
988 .map_err(map_sqlx)?;
989
990 match row {
991 Some(r) => Ok(Some(row_to_event(&r).map_err(map_sqlx)?)),
992 None => Ok(None),
993 }
994 }
995
996 async fn list_child_events(
997 &self,
998 parent_event_id: Uuid,
999 limit: usize,
1000 ) -> Result<Vec<AgentEvent>> {
1001 let rows = sqlx::query(
1002 r#"
1003SELECT id, agent_id, thread_id, run_id, parent_event_id, event_type,
1004 payload, trace_id, span_id, model, tokens_input, tokens_output,
1005 latency_ms, cost_usd, "timestamp", logical_clock, content_hash,
1006 prev_hash, embedding
1007FROM agent_events
1008WHERE parent_event_id = $1
1009ORDER BY "timestamp" ASC
1010LIMIT $2
1011"#,
1012 )
1013 .bind(parent_event_id)
1014 .bind(limit as i64)
1015 .fetch_all(&self.pool)
1016 .await
1017 .map_err(map_sqlx)?;
1018
1019 let mut results = Vec::with_capacity(rows.len());
1020 for r in &rows {
1021 results.push(row_to_event(r).map_err(map_sqlx)?);
1022 }
1023 Ok(results)
1024 }
1025
1026 async fn list_memories_by_agent_ordered(
1031 &self,
1032 agent_id: &str,
1033 thread_id: Option<&str>,
1034 limit: usize,
1035 ) -> Result<Vec<MemoryRecord>> {
1036 let rows = if let Some(tid) = thread_id {
1037 let sql = format!(
1038 "SELECT {MEMORY_COLUMNS} FROM memories WHERE agent_id = $1 AND thread_id = $2 AND deleted_at IS NULL ORDER BY created_at ASC LIMIT $3"
1039 );
1040 sqlx::query(sqlx::AssertSqlSafe(sql.as_str()))
1041 .bind(agent_id)
1042 .bind(tid)
1043 .bind(limit as i64)
1044 .fetch_all(&self.pool)
1045 .await
1046 .map_err(map_sqlx)?
1047 } else {
1048 let sql = format!(
1049 "SELECT {MEMORY_COLUMNS} FROM memories WHERE agent_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC LIMIT $2"
1050 );
1051 sqlx::query(sqlx::AssertSqlSafe(sql.as_str()))
1052 .bind(agent_id)
1053 .bind(limit as i64)
1054 .fetch_all(&self.pool)
1055 .await
1056 .map_err(map_sqlx)?
1057 };
1058
1059 let mut results = Vec::with_capacity(rows.len());
1060 for r in &rows {
1061 results.push(row_to_memory(r).map_err(map_sqlx)?);
1062 }
1063 Ok(results)
1064 }
1065
1066 async fn list_memories_since(
1071 &self,
1072 updated_after: &str,
1073 limit: usize,
1074 ) -> Result<Vec<MemoryRecord>> {
1075 let sql = format!(
1076 "SELECT {MEMORY_COLUMNS} FROM memories WHERE updated_at > $1 ORDER BY updated_at ASC LIMIT $2"
1077 );
1078 let rows = sqlx::query(sqlx::AssertSqlSafe(sql.as_str()))
1079 .bind(updated_after)
1080 .bind(limit as i64)
1081 .fetch_all(&self.pool)
1082 .await
1083 .map_err(map_sqlx)?;
1084
1085 let mut results = Vec::with_capacity(rows.len());
1086 for r in &rows {
1087 results.push(row_to_memory(r).map_err(map_sqlx)?);
1088 }
1089 Ok(results)
1090 }
1091
1092 async fn upsert_memory(&self, record: &MemoryRecord) -> Result<()> {
1093 match self.update_memory(record).await {
1094 Ok(()) => Ok(()),
1095 Err(Error::NotFound(_)) => self.insert_memory(record).await,
1096 Err(e) => Err(e),
1097 }
1098 }
1099
1100 async fn cleanup_expired(&self) -> Result<usize> {
1105 let now = chrono::Utc::now().to_rfc3339();
1106 let result = sqlx::query(
1107 "UPDATE memories SET deleted_at = $1 WHERE expires_at IS NOT NULL AND expires_at < $2 AND deleted_at IS NULL",
1108 )
1109 .bind(&now)
1110 .bind(&now)
1111 .execute(&self.pool)
1112 .await
1113 .map_err(map_sqlx)?;
1114
1115 Ok(result.rows_affected() as usize)
1116 }
1117
1118 async fn insert_delegation(&self, d: &Delegation) -> Result<()> {
1123 let scope_type = d.scope.to_string();
1124 let scope_value: serde_json::Value = match &d.scope {
1125 DelegationScope::AllMemories => serde_json::Value::Null,
1126 DelegationScope::ByTag(tags) => serde_json::json!(tags),
1127 DelegationScope::ByMemoryId(ids) => {
1128 serde_json::json!(ids.iter().map(|id| id.to_string()).collect::<Vec<_>>())
1129 }
1130 };
1131
1132 sqlx::query(
1133 r#"
1134INSERT INTO delegations (
1135 id, delegator_id, delegate_id, permission, scope_type, scope_value,
1136 max_depth, current_depth, parent_delegation_id,
1137 created_at, expires_at, revoked_at
1138) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1139"#,
1140 )
1141 .bind(d.id)
1142 .bind(&d.delegator_id)
1143 .bind(&d.delegate_id)
1144 .bind(d.permission.to_string())
1145 .bind(&scope_type)
1146 .bind(&scope_value)
1147 .bind(d.max_depth as i32)
1148 .bind(d.current_depth as i32)
1149 .bind(d.parent_delegation_id)
1150 .bind(&d.created_at)
1151 .bind(&d.expires_at)
1152 .bind(&d.revoked_at)
1153 .execute(&self.pool)
1154 .await
1155 .map_err(map_sqlx)?;
1156 Ok(())
1157 }
1158
1159 async fn list_delegations_for(&self, delegate_id: &str) -> Result<Vec<Delegation>> {
1160 let now = chrono::Utc::now().to_rfc3339();
1161 let rows = sqlx::query(
1162 r#"
1163SELECT id, delegator_id, delegate_id, permission, scope_type, scope_value,
1164 max_depth, current_depth, parent_delegation_id,
1165 created_at, expires_at, revoked_at
1166FROM delegations
1167WHERE delegate_id = $1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > $2)
1168"#,
1169 )
1170 .bind(delegate_id)
1171 .bind(&now)
1172 .fetch_all(&self.pool)
1173 .await
1174 .map_err(map_sqlx)?;
1175
1176 let mut results = Vec::with_capacity(rows.len());
1177 for r in &rows {
1178 results.push(row_to_delegation(r).map_err(map_sqlx)?);
1179 }
1180 Ok(results)
1181 }
1182
1183 async fn revoke_delegation(&self, id: Uuid) -> Result<()> {
1184 let now = chrono::Utc::now().to_rfc3339();
1185 let result = sqlx::query(
1186 "UPDATE delegations SET revoked_at = $1 WHERE id = $2 AND revoked_at IS NULL",
1187 )
1188 .bind(&now)
1189 .bind(id)
1190 .execute(&self.pool)
1191 .await
1192 .map_err(map_sqlx)?;
1193
1194 if result.rows_affected() == 0 {
1195 return Err(Error::NotFound(format!(
1196 "delegation {id} not found or already revoked"
1197 )));
1198 }
1199 Ok(())
1200 }
1201
1202 async fn check_delegation(
1203 &self,
1204 delegate_id: &str,
1205 memory_id: Uuid,
1206 required: Permission,
1207 ) -> Result<bool> {
1208 let delegations = self.list_delegations_for(delegate_id).await?;
1209
1210 let memory = match self.get_memory(memory_id).await? {
1212 Some(m) => m,
1213 None => return Ok(false),
1214 };
1215
1216 for d in &delegations {
1217 if !d.permission.satisfies(required) {
1218 continue;
1219 }
1220 match &d.scope {
1221 DelegationScope::AllMemories => return Ok(true),
1222 DelegationScope::ByMemoryId(ids) => {
1223 if ids.contains(&memory_id) {
1224 return Ok(true);
1225 }
1226 }
1227 DelegationScope::ByTag(tags) => {
1228 if tags.iter().any(|t| memory.tags.contains(t)) {
1229 return Ok(true);
1230 }
1231 }
1232 }
1233 }
1234 Ok(false)
1235 }
1236
1237 async fn insert_or_update_agent_profile(&self, profile: &AgentProfile) -> Result<()> {
1242 sqlx::query(
1243 r#"
1244INSERT INTO agent_profiles (agent_id, avg_importance, avg_content_length, total_memories, last_updated)
1245VALUES ($1, $2, $3, $4, $5)
1246ON CONFLICT (agent_id) DO UPDATE SET
1247 avg_importance = EXCLUDED.avg_importance,
1248 avg_content_length = EXCLUDED.avg_content_length,
1249 total_memories = EXCLUDED.total_memories,
1250 last_updated = EXCLUDED.last_updated
1251"#,
1252 )
1253 .bind(&profile.agent_id)
1254 .bind(profile.avg_importance)
1255 .bind(profile.avg_content_length)
1256 .bind(profile.total_memories as i64)
1257 .bind(&profile.last_updated)
1258 .execute(&self.pool)
1259 .await
1260 .map_err(map_sqlx)?;
1261 Ok(())
1262 }
1263
1264 async fn get_agent_profile(&self, agent_id: &str) -> Result<Option<AgentProfile>> {
1265 let row = sqlx::query(
1266 "SELECT agent_id, avg_importance, avg_content_length, total_memories, last_updated FROM agent_profiles WHERE agent_id = $1",
1267 )
1268 .bind(agent_id)
1269 .fetch_optional(&self.pool)
1270 .await
1271 .map_err(map_sqlx)?;
1272
1273 Ok(row.map(|r| AgentProfile {
1274 agent_id: r.get("agent_id"),
1275 avg_importance: r.get("avg_importance"),
1276 avg_content_length: r.get("avg_content_length"),
1277 total_memories: r.get::<i64, _>("total_memories") as u64,
1278 last_updated: r.get("last_updated"),
1279 }))
1280 }
1281
1282 async fn insert_or_update_embedding_baseline(
1287 &self,
1288 baseline: &EmbeddingBaseline,
1289 ) -> Result<()> {
1290 let mu_json =
1291 serde_json::to_value(&baseline.mu).map_err(|e| Error::Storage(e.to_string()))?;
1292 let cov_json =
1293 serde_json::to_value(&baseline.cov_diag).map_err(|e| Error::Storage(e.to_string()))?;
1294 sqlx::query(
1295 r#"
1296INSERT INTO embedding_baseline (agent_id, mu, cov_diag, n, updated_at)
1297VALUES ($1, $2, $3, $4, $5)
1298ON CONFLICT (agent_id) DO UPDATE SET
1299 mu = EXCLUDED.mu,
1300 cov_diag = EXCLUDED.cov_diag,
1301 n = EXCLUDED.n,
1302 updated_at = EXCLUDED.updated_at
1303"#,
1304 )
1305 .bind(&baseline.agent_id)
1306 .bind(&mu_json)
1307 .bind(&cov_json)
1308 .bind(baseline.n as i64)
1309 .bind(&baseline.updated_at)
1310 .execute(&self.pool)
1311 .await
1312 .map_err(map_sqlx)?;
1313 Ok(())
1314 }
1315
1316 async fn get_embedding_baseline(&self, agent_id: &str) -> Result<Option<EmbeddingBaseline>> {
1317 let row = sqlx::query(
1318 "SELECT agent_id, mu, cov_diag, n, updated_at FROM embedding_baseline WHERE agent_id = $1",
1319 )
1320 .bind(agent_id)
1321 .fetch_optional(&self.pool)
1322 .await
1323 .map_err(map_sqlx)?;
1324
1325 match row {
1326 None => Ok(None),
1327 Some(r) => {
1328 let mu_val: serde_json::Value = r.get("mu");
1329 let cov_val: serde_json::Value = r.get("cov_diag");
1330 let mu: Vec<f32> =
1331 serde_json::from_value(mu_val).map_err(|e| Error::Storage(e.to_string()))?;
1332 let cov_diag: Vec<f32> =
1333 serde_json::from_value(cov_val).map_err(|e| Error::Storage(e.to_string()))?;
1334 Ok(Some(EmbeddingBaseline {
1335 agent_id: r.get("agent_id"),
1336 mu,
1337 cov_diag,
1338 n: r.get::<i64, _>("n") as u64,
1339 updated_at: r.get("updated_at"),
1340 }))
1341 }
1342 }
1343 }
1344
1345 async fn insert_checkpoint(&self, cp: &Checkpoint) -> Result<()> {
1350 let memory_refs_strs: Vec<String> =
1351 cp.memory_refs.iter().map(|id| id.to_string()).collect();
1352 let refs_slice: &[String] = &memory_refs_strs;
1353
1354 sqlx::query(
1355 r#"
1356INSERT INTO checkpoints (
1357 id, thread_id, agent_id, parent_id, branch_name,
1358 state_snapshot, state_diff, memory_refs, event_cursor,
1359 label, created_at, metadata
1360) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1361"#,
1362 )
1363 .bind(cp.id)
1364 .bind(&cp.thread_id)
1365 .bind(&cp.agent_id)
1366 .bind(cp.parent_id)
1367 .bind(&cp.branch_name)
1368 .bind(&cp.state_snapshot)
1369 .bind(&cp.state_diff)
1370 .bind(refs_slice)
1371 .bind(cp.event_cursor)
1372 .bind(&cp.label)
1373 .bind(&cp.created_at)
1374 .bind(&cp.metadata)
1375 .execute(&self.pool)
1376 .await
1377 .map_err(map_sqlx)?;
1378 Ok(())
1379 }
1380
1381 async fn get_checkpoint(&self, id: Uuid) -> Result<Option<Checkpoint>> {
1382 let row = sqlx::query(
1383 r#"
1384SELECT id, thread_id, agent_id, parent_id, branch_name,
1385 state_snapshot, state_diff, memory_refs, event_cursor,
1386 label, created_at, metadata
1387FROM checkpoints WHERE id = $1
1388"#,
1389 )
1390 .bind(id)
1391 .fetch_optional(&self.pool)
1392 .await
1393 .map_err(map_sqlx)?;
1394
1395 match row {
1396 Some(r) => Ok(Some(row_to_checkpoint(&r).map_err(map_sqlx)?)),
1397 None => Ok(None),
1398 }
1399 }
1400
1401 async fn list_checkpoints(
1402 &self,
1403 thread_id: &str,
1404 branch: Option<&str>,
1405 limit: usize,
1406 ) -> Result<Vec<Checkpoint>> {
1407 let rows = if let Some(branch_name) = branch {
1408 sqlx::query(
1409 r#"
1410SELECT id, thread_id, agent_id, parent_id, branch_name,
1411 state_snapshot, state_diff, memory_refs, event_cursor,
1412 label, created_at, metadata
1413FROM checkpoints
1414WHERE thread_id = $1 AND branch_name = $2
1415ORDER BY created_at DESC
1416LIMIT $3
1417"#,
1418 )
1419 .bind(thread_id)
1420 .bind(branch_name)
1421 .bind(limit as i64)
1422 .fetch_all(&self.pool)
1423 .await
1424 .map_err(map_sqlx)?
1425 } else {
1426 sqlx::query(
1427 r#"
1428SELECT id, thread_id, agent_id, parent_id, branch_name,
1429 state_snapshot, state_diff, memory_refs, event_cursor,
1430 label, created_at, metadata
1431FROM checkpoints
1432WHERE thread_id = $1
1433ORDER BY created_at DESC
1434LIMIT $2
1435"#,
1436 )
1437 .bind(thread_id)
1438 .bind(limit as i64)
1439 .fetch_all(&self.pool)
1440 .await
1441 .map_err(map_sqlx)?
1442 };
1443
1444 let mut results = Vec::with_capacity(rows.len());
1445 for r in &rows {
1446 results.push(row_to_checkpoint(r).map_err(map_sqlx)?);
1447 }
1448 Ok(results)
1449 }
1450
1451 async fn get_latest_checkpoint(
1452 &self,
1453 thread_id: &str,
1454 branch: &str,
1455 ) -> Result<Option<Checkpoint>> {
1456 let row = sqlx::query(
1457 r#"
1458SELECT id, thread_id, agent_id, parent_id, branch_name,
1459 state_snapshot, state_diff, memory_refs, event_cursor,
1460 label, created_at, metadata
1461FROM checkpoints
1462WHERE thread_id = $1 AND branch_name = $2
1463ORDER BY created_at DESC
1464LIMIT 1
1465"#,
1466 )
1467 .bind(thread_id)
1468 .bind(branch)
1469 .fetch_optional(&self.pool)
1470 .await
1471 .map_err(map_sqlx)?;
1472
1473 match row {
1474 Some(r) => Ok(Some(row_to_checkpoint(&r).map_err(map_sqlx)?)),
1475 None => Ok(None),
1476 }
1477 }
1478}