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