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