1use std::cell::{Cell, RefCell};
9use std::collections::HashSet;
10use std::rc::Rc;
11
12use serde_json::{json, Value};
13
14use crate::graph::{Graph, GraphNodeOpts};
15use crate::json::{strict_canonical_json_bytes, strict_json_decode, Codec, JsonValue};
16use crate::node::Node;
17use crate::protocol::Message;
18use crate::solutions::{
19 agentic_memory_record_frame, agentic_memory_record_frame_codec, AgenticMemoryRecord,
20 AgenticMemoryRecordFrame,
21};
22use crate::storage::{
23 AppendLogReadOptions, AppendLogStorageTier, KvStorageTier, StorageError, StorageResult,
24};
25
26type Disposer = Box<dyn FnOnce()>;
27
28pub const AGENTIC_MEMORY_RECORD_SNAPSHOT_FORMAT: &str = "graphrefly.agenticMemory.records.snapshot";
30pub const AGENTIC_MEMORY_RECORD_CHANGE_FORMAT: &str = "graphrefly.agenticMemory.records.change";
32pub const AGENTIC_MEMORY_RECORD_STORAGE_FRAME_VERSION: u32 = 1;
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum AgenticMemoryRecordsPersistenceStatus {
38 Starting,
40 Ready,
42 Flushing,
44 Errored,
46 Disposed,
48}
49
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct AgenticMemoryRecordsPersistenceCursor {
53 pub change_seq: Option<u64>,
55 pub snapshot_writes: usize,
57 pub change_writes: usize,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct AgenticMemoryRecordsPersistenceStatusFact {
64 pub state: AgenticMemoryRecordsPersistenceStatus,
66 pub pending: usize,
68 pub writes: usize,
70 pub errors: usize,
72 pub cursor: AgenticMemoryRecordsPersistenceCursor,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct AgenticMemoryRecordsPersistenceErrorFact {
79 pub phase: String,
81 pub message: String,
83 pub cursor: AgenticMemoryRecordsPersistenceCursor,
85}
86
87#[derive(Clone, Debug, PartialEq)]
88pub struct AgenticMemoryRecordsRestoreState {
90 pub records: Vec<AgenticMemoryRecord<JsonValue>>,
92 pub snapshot_found: bool,
94 pub changes_applied: usize,
96 pub cursor: Option<u64>,
98 pub source: String,
100}
101
102#[derive(Clone, Default)]
103pub struct LoadAgenticMemoryRecordsStateOptions<'a> {
105 pub storage_prefix: Option<&'a str>,
107 pub snapshot_key: Option<&'a str>,
109 pub change_log: Option<&'a dyn AppendLogStorageTier<Value>>,
111}
112
113#[derive(Clone)]
114pub struct PersistAgenticMemoryRecordsOptions {
116 pub graph: Option<Graph>,
118 pub name: Option<String>,
120 pub storage_prefix: Option<String>,
122 pub snapshot_key: Option<String>,
124 pub snapshot_store: Rc<dyn KvStorageTier<Value>>,
126 pub change_log: Option<Rc<dyn AppendLogStorageTier<Value>>>,
132 pub snapshot_on_attach: bool,
134}
135
136impl PersistAgenticMemoryRecordsOptions {
137 pub fn new(
139 snapshot_store: Rc<dyn KvStorageTier<Value>>,
140 storage_prefix: impl Into<String>,
141 ) -> Self {
142 Self {
143 graph: None,
144 name: None,
145 storage_prefix: Some(storage_prefix.into()),
146 snapshot_key: None,
147 snapshot_store,
148 change_log: None,
149 snapshot_on_attach: true,
150 }
151 }
152}
153
154pub struct AgenticMemoryRecordsPersistence {
156 pub ready: Node<bool>,
158 pub status: Node<Value>,
160 pub error: Node<Value>,
162 pub cursor: Node<Value>,
164 flush: Rc<dyn Fn() -> StorageResult<()>>,
165 snapshot: Rc<dyn Fn() -> StorageResult<()>>,
166 dispose: RefCell<Option<Disposer>>,
167}
168
169impl AgenticMemoryRecordsPersistence {
170 pub fn flush(&self) -> StorageResult<()> {
172 (self.flush)()
173 }
174
175 pub fn snapshot(&self) -> StorageResult<()> {
177 (self.snapshot)()
178 }
179
180 pub fn dispose(&self) {
182 if let Some(dispose) = self.dispose.borrow_mut().take() {
183 dispose();
184 }
185 }
186
187 pub fn cursor_fact(&self) -> StorageResult<AgenticMemoryRecordsPersistenceCursor> {
189 parse_cursor_fact(
190 &self.cursor.cache().ok_or_else(|| {
191 StorageError::backend("agenticMemoryRecords cursor fact is absent")
192 })?,
193 )
194 }
195}
196
197impl Drop for AgenticMemoryRecordsPersistence {
198 fn drop(&mut self) {
199 if let Some(dispose) = self.dispose.borrow_mut().take() {
200 dispose();
201 }
202 }
203}
204
205pub struct OpenPersistentAgenticMemoryRecords {
207 pub records: Node<Vec<AgenticMemoryRecord<JsonValue>>>,
209 pub persistence: AgenticMemoryRecordsPersistence,
211 pub loaded: AgenticMemoryRecordsRestoreState,
213}
214
215pub struct OpenPersistentAgenticMemoryRecordsOptions {
217 pub graph: Graph,
219 pub name: Option<String>,
221 pub initial: Vec<AgenticMemoryRecord<JsonValue>>,
223 pub persistence: PersistAgenticMemoryRecordsOptions,
225}
226
227pub fn agentic_memory_records_snapshot_key(storage_prefix: &str) -> StorageResult<String> {
229 if storage_prefix.is_empty() {
230 return Err(StorageError::backend("storage_prefix must be non-empty"));
231 }
232 Ok(format!("{storage_prefix}/records.snapshot"))
233}
234
235pub fn agentic_memory_record_snapshot_frame(
237 records: &[AgenticMemoryRecord<JsonValue>],
238 change_cursor: Option<u64>,
239) -> StorageResult<Value> {
240 let cursor = change_cursor
241 .map(|seq| i64::try_from(seq).map_err(|_| StorageError::backend("change cursor overflow")))
242 .transpose()?
243 .unwrap_or(-1);
244 let records = records
245 .iter()
246 .map(record_frame_json)
247 .collect::<StorageResult<Vec<_>>>()?;
248 Ok(json!({
249 "format": AGENTIC_MEMORY_RECORD_SNAPSHOT_FORMAT,
250 "version": AGENTIC_MEMORY_RECORD_STORAGE_FRAME_VERSION,
251 "changeCursor": cursor,
252 "records": records,
253 }))
254}
255
256pub fn agentic_memory_record_change_frame(
258 records: &[AgenticMemoryRecord<JsonValue>],
259) -> StorageResult<Value> {
260 let records = records
261 .iter()
262 .map(record_frame_json)
263 .collect::<StorageResult<Vec<_>>>()?;
264 Ok(json!({
265 "format": AGENTIC_MEMORY_RECORD_CHANGE_FORMAT,
266 "version": AGENTIC_MEMORY_RECORD_STORAGE_FRAME_VERSION,
267 "change": { "kind": "replaceAll", "records": records },
268 }))
269}
270
271pub fn load_agentic_memory_records_state(
273 snapshot_store: &dyn KvStorageTier<Value>,
274 options: LoadAgenticMemoryRecordsStateOptions<'_>,
275) -> StorageResult<AgenticMemoryRecordsRestoreState> {
276 let storage_prefix = options.storage_prefix.unwrap_or("agentic-memory");
277 let snapshot_key = options
278 .snapshot_key
279 .map(ToOwned::to_owned)
280 .unwrap_or(agentic_memory_records_snapshot_key(storage_prefix)?);
281 let snapshot = snapshot_store.get(&snapshot_key)?;
282 let mut records = Vec::new();
283 let mut snapshot_found = false;
284 let mut cursor = None;
285 if let Some(snapshot) = snapshot {
286 let parsed = parse_snapshot_frame(&snapshot)?;
287 records = parsed.records;
288 snapshot_found = true;
289 cursor = parsed.change_cursor;
290 }
291 let mut changes_applied = 0usize;
292 if let Some(change_log) = options.change_log {
293 let entries = change_log.read(AppendLogReadOptions::default())?;
294 let mut expected = cursor.map(|seq| seq.saturating_add(1)).unwrap_or(0);
295 for entry in entries {
296 if let Some(snapshot_cursor) = cursor {
297 if entry.seq <= snapshot_cursor {
298 continue;
299 }
300 }
301 if entry.seq != expected {
302 return Err(StorageError::backend(
303 "agentic memory records change log is non-contiguous",
304 ));
305 }
306 let change = parse_change_frame(&entry.value)?;
307 records = change;
308 cursor = Some(entry.seq);
309 expected = entry.seq.saturating_add(1);
310 changes_applied += 1;
311 }
312 }
313 Ok(AgenticMemoryRecordsRestoreState {
314 records,
315 snapshot_found,
316 changes_applied,
317 cursor,
318 source: match (snapshot_found, changes_applied > 0) {
319 (true, true) => "snapshot+changes",
320 (true, false) => "snapshot",
321 (false, true) => "changes",
322 (false, false) => "empty",
323 }
324 .to_owned(),
325 })
326}
327
328pub fn persist_agentic_memory_records(
330 records: &Node<Vec<AgenticMemoryRecord<JsonValue>>>,
331 options: PersistAgenticMemoryRecordsOptions,
332) -> StorageResult<AgenticMemoryRecordsPersistence> {
333 persist_agentic_memory_records_with_cursor(records, options, None)
334}
335
336fn persist_agentic_memory_records_with_cursor(
337 records: &Node<Vec<AgenticMemoryRecord<JsonValue>>>,
338 options: PersistAgenticMemoryRecordsOptions,
339 initial_change_seq: Option<u64>,
340) -> StorageResult<AgenticMemoryRecordsPersistence> {
341 let graph = options.graph.as_ref().ok_or_else(|| {
342 StorageError::backend(
343 "persistAgenticMemoryRecords: graph is required for graph-visible sidecar facts",
344 )
345 })?;
346 if !graph.contains_core(&records.erased()) {
347 return Err(StorageError::backend(
348 "persistAgenticMemoryRecords: records node belongs to a different graph or is not graph-registered",
349 ));
350 }
351 let prefix = options
352 .name
353 .clone()
354 .or_else(|| options.storage_prefix.clone())
355 .unwrap_or_else(|| "agenticMemoryRecords.persistence".to_owned());
356 let snapshot_key = options
357 .snapshot_key
358 .clone()
359 .unwrap_or(agentic_memory_records_snapshot_key(
360 options.storage_prefix.as_deref().unwrap_or(&prefix),
361 )?);
362 let ready = graph.state_opts(false, GraphNodeOpts::named(format!("{prefix}.ready")));
363 let status = graph.state_opts(
364 status_json(
365 AgenticMemoryRecordsPersistenceStatus::Starting,
366 0,
367 0,
368 cursor_json(initial_change_seq, 0, 0),
369 ),
370 GraphNodeOpts::named(format!("{prefix}.status")),
371 );
372 let error = graph.state_opts(Value::Null, GraphNodeOpts::named(format!("{prefix}.error")));
373 let cursor = graph.state_opts(
374 cursor_json(initial_change_seq, 0, 0),
375 GraphNodeOpts::named(format!("{prefix}.cursor")),
376 );
377 let latest = Rc::new(RefCell::new(records.cache().unwrap_or_default()));
378 let change_seq = Rc::new(Cell::new(initial_change_seq));
379 let snapshot_writes = Rc::new(Cell::new(0usize));
380 let change_writes = Rc::new(Cell::new(0usize));
381 let error_count = Rc::new(Cell::new(0usize));
382 let disposed = Rc::new(Cell::new(false));
383 let failed_write = Rc::new(RefCell::new(None::<StorageError>));
384 let snapshot_store = options.snapshot_store.clone();
385 let change_log = options.change_log.clone();
386
387 let write_snapshot = {
388 let latest = latest.clone();
389 let snapshot_store = snapshot_store.clone();
390 let snapshot_key = snapshot_key.clone();
391 let change_seq = change_seq.clone();
392 let snapshot_writes = snapshot_writes.clone();
393 let change_writes = change_writes.clone();
394 let ready = ready.clone();
395 let status = status.clone();
396 let error = error.clone();
397 let cursor = cursor.clone();
398 let failed_write = failed_write.clone();
399 move || {
400 let frame = agentic_memory_record_snapshot_frame(&latest.borrow(), change_seq.get())?;
401 snapshot_store.set(&snapshot_key, frame)?;
402 failed_write.replace(None);
403 snapshot_writes.set(snapshot_writes.get().saturating_add(1));
404 let cursor_value =
405 cursor_json(change_seq.get(), snapshot_writes.get(), change_writes.get());
406 ready.set(true);
407 status.set(status_json(
408 AgenticMemoryRecordsPersistenceStatus::Ready,
409 0,
410 0,
411 cursor_value.clone(),
412 ));
413 error.set(Value::Null);
414 cursor.set(cursor_value);
415 Ok(())
416 }
417 };
418
419 if options.snapshot_on_attach {
420 write_snapshot()?;
421 } else {
422 ready.set(true);
423 status.set(status_json(
424 AgenticMemoryRecordsPersistenceStatus::Ready,
425 0,
426 0,
427 cursor_json(initial_change_seq, 0, 0),
428 ));
429 }
430
431 let write_snapshot_for_control = Rc::new(write_snapshot);
432 let subscribing = Rc::new(Cell::new(true));
433 let unsubscribe = {
434 let latest = latest.clone();
435 let change_log = change_log.clone();
436 let change_seq = change_seq.clone();
437 let snapshot_writes = snapshot_writes.clone();
438 let change_writes = change_writes.clone();
439 let ready = ready.clone();
440 let status = status.clone();
441 let error = error.clone();
442 let cursor = cursor.clone();
443 let error_count = error_count.clone();
444 let failed_write = failed_write.clone();
445 let subscribing = subscribing.clone();
446 records.subscribe(move |message| {
447 let Message::Data(next) = message else {
448 return;
449 };
450 let Some(next) = next
451 .as_ref()
452 .downcast_ref::<Vec<AgenticMemoryRecord<JsonValue>>>()
453 else {
454 return;
455 };
456 latest.replace(next.clone());
457 if subscribing.get() {
458 subscribing.set(false);
459 return;
460 }
461 let Some(change_log) = &change_log else {
462 return;
463 };
464 match agentic_memory_record_change_frame(next)
465 .and_then(|frame| change_log.append(frame).map(|entry| entry.seq))
466 {
467 Ok(seq) => {
468 failed_write.replace(None);
469 change_seq.set(Some(seq));
470 change_writes.set(change_writes.get().saturating_add(1));
471 let cursor_value =
472 cursor_json(change_seq.get(), snapshot_writes.get(), change_writes.get());
473 ready.set(true);
474 status.set(status_json(
475 AgenticMemoryRecordsPersistenceStatus::Ready,
476 0,
477 error_count.get(),
478 cursor_value.clone(),
479 ));
480 error.set(Value::Null);
481 cursor.set(cursor_value);
482 }
483 Err(err) => {
484 failed_write.replace(Some(err.clone()));
485 error_count.set(error_count.get().saturating_add(1));
486 ready.set(false);
487 let cursor_value =
488 cursor_json(change_seq.get(), snapshot_writes.get(), change_writes.get());
489 status.set(status_json(
490 AgenticMemoryRecordsPersistenceStatus::Errored,
491 0,
492 error_count.get(),
493 cursor_value.clone(),
494 ));
495 error.set(error_json("change", &err, cursor_value));
496 }
497 }
498 })
499 };
500 subscribing.set(false);
501 let flush = {
502 let status = status.clone();
503 let cursor = cursor.clone();
504 let ready = ready.clone();
505 let change_seq = change_seq.clone();
506 let snapshot_writes = snapshot_writes.clone();
507 let change_writes = change_writes.clone();
508 let error_count = error_count.clone();
509 let disposed = disposed.clone();
510 let error = error.clone();
511 let failed_write = failed_write.clone();
512 Rc::new(move || {
513 if disposed.get() {
514 return Ok(());
515 }
516 let cursor_value =
517 cursor_json(change_seq.get(), snapshot_writes.get(), change_writes.get());
518 if let Some(err) = failed_write.borrow().clone() {
519 ready.set(false);
520 status.set(status_json(
521 AgenticMemoryRecordsPersistenceStatus::Errored,
522 0,
523 error_count.get(),
524 cursor_value.clone(),
525 ));
526 error.set(error_json("flush", &err, cursor_value));
527 return Err(err);
528 }
529 ready.set(true);
530 status.set(status_json(
531 AgenticMemoryRecordsPersistenceStatus::Ready,
532 0,
533 error_count.get(),
534 cursor_value.clone(),
535 ));
536 cursor.set(cursor_value);
537 Ok(())
538 })
539 };
540 let snapshot = {
541 let write_snapshot = write_snapshot_for_control.clone();
542 let disposed = disposed.clone();
543 Rc::new(move || {
544 if disposed.get() {
545 return Err(StorageError::backend(
546 "agenticMemoryRecords persistence is disposed",
547 ));
548 }
549 write_snapshot()
550 })
551 };
552 let dispose = {
553 let disposed = disposed.clone();
554 let ready = ready.clone();
555 let status = status.clone();
556 let cursor = cursor.clone();
557 let change_seq = change_seq.clone();
558 let snapshot_writes = snapshot_writes.clone();
559 let change_writes = change_writes.clone();
560 Box::new(move || {
561 disposed.set(true);
562 unsubscribe();
563 ready.set(false);
564 status.set(status_json(
565 AgenticMemoryRecordsPersistenceStatus::Disposed,
566 0,
567 0,
568 cursor_json(change_seq.get(), snapshot_writes.get(), change_writes.get()),
569 ));
570 cursor.set(cursor_json(
571 change_seq.get(),
572 snapshot_writes.get(),
573 change_writes.get(),
574 ));
575 }) as Disposer
576 };
577
578 Ok(AgenticMemoryRecordsPersistence {
579 ready,
580 status,
581 error,
582 cursor,
583 flush,
584 snapshot,
585 dispose: RefCell::new(Some(dispose)),
586 })
587}
588
589pub fn open_persistent_agentic_memory_records(
591 options: OpenPersistentAgenticMemoryRecordsOptions,
592) -> StorageResult<OpenPersistentAgenticMemoryRecords> {
593 let snapshot_key =
594 options
595 .persistence
596 .snapshot_key
597 .clone()
598 .or(agentic_memory_records_snapshot_key(
599 options
600 .persistence
601 .storage_prefix
602 .as_deref()
603 .unwrap_or("agentic-memory"),
604 )
605 .ok());
606 let mut loaded = load_agentic_memory_records_state(
607 options.persistence.snapshot_store.as_ref(),
608 LoadAgenticMemoryRecordsStateOptions {
609 storage_prefix: options.persistence.storage_prefix.as_deref(),
610 snapshot_key: snapshot_key.as_deref(),
611 change_log: options.persistence.change_log.as_deref(),
612 },
613 )?;
614 if loaded.source == "empty" {
615 loaded.records = options.initial;
616 }
617 let records = options.graph.state_opts(
618 loaded.records.clone(),
619 GraphNodeOpts::named(
620 options
621 .name
622 .clone()
623 .unwrap_or_else(|| "agenticMemoryRecords".to_owned()),
624 ),
625 );
626 let mut persistence_options = options.persistence;
627 if persistence_options.graph.is_none() {
628 persistence_options.graph = Some(options.graph.clone());
629 }
630 let persistence =
631 persist_agentic_memory_records_with_cursor(&records, persistence_options, loaded.cursor)?;
632 Ok(OpenPersistentAgenticMemoryRecords {
633 records,
634 persistence,
635 loaded,
636 })
637}
638
639struct ParsedSnapshot {
640 records: Vec<AgenticMemoryRecord<JsonValue>>,
641 change_cursor: Option<u64>,
642}
643
644fn parse_snapshot_frame(value: &Value) -> StorageResult<ParsedSnapshot> {
645 let object = value
646 .as_object()
647 .ok_or_else(|| StorageError::backend("agentic memory snapshot frame must be an object"))?;
648 assert_keys(
649 object.keys().map(String::as_str),
650 ["changeCursor", "format", "records", "version"],
651 )?;
652 if object.get("format")
653 != Some(&Value::String(
654 AGENTIC_MEMORY_RECORD_SNAPSHOT_FORMAT.to_owned(),
655 ))
656 {
657 return Err(StorageError::backend(
658 "agentic memory snapshot frame: invalid format",
659 ));
660 }
661 if object.get("version").and_then(Value::as_u64)
662 != Some(AGENTIC_MEMORY_RECORD_STORAGE_FRAME_VERSION as u64)
663 {
664 return Err(StorageError::backend(
665 "agentic memory snapshot frame: invalid version",
666 ));
667 }
668 let change_cursor = cursor_from_json(object.get("changeCursor"))?;
669 let records = object
670 .get("records")
671 .and_then(Value::as_array)
672 .ok_or_else(|| {
673 StorageError::backend("agentic memory snapshot frame: records must be an array")
674 })?
675 .iter()
676 .map(record_from_frame_json)
677 .collect::<StorageResult<Vec<_>>>()?;
678 validate_unique_record_set(&records, "agentic memory snapshot frame")?;
679 Ok(ParsedSnapshot {
680 records,
681 change_cursor,
682 })
683}
684
685fn parse_change_frame(value: &Value) -> StorageResult<Vec<AgenticMemoryRecord<JsonValue>>> {
686 let object = value
687 .as_object()
688 .ok_or_else(|| StorageError::backend("agentic memory change frame must be an object"))?;
689 assert_keys(
690 object.keys().map(String::as_str),
691 ["change", "format", "version"],
692 )?;
693 if object.get("format")
694 != Some(&Value::String(
695 AGENTIC_MEMORY_RECORD_CHANGE_FORMAT.to_owned(),
696 ))
697 {
698 return Err(StorageError::backend(
699 "agentic memory change frame: invalid format",
700 ));
701 }
702 if object.get("version").and_then(Value::as_u64)
703 != Some(AGENTIC_MEMORY_RECORD_STORAGE_FRAME_VERSION as u64)
704 {
705 return Err(StorageError::backend(
706 "agentic memory change frame: invalid version",
707 ));
708 }
709 let change = object
710 .get("change")
711 .and_then(Value::as_object)
712 .ok_or_else(|| {
713 StorageError::backend("agentic memory change frame: change must be an object")
714 })?;
715 assert_keys(change.keys().map(String::as_str), ["kind", "records"])?;
716 if change.get("kind") != Some(&Value::String("replaceAll".to_owned())) {
717 return Err(StorageError::backend(
718 "agentic memory change frame: change.kind must be replaceAll",
719 ));
720 }
721 let records = change
722 .get("records")
723 .and_then(Value::as_array)
724 .ok_or_else(|| {
725 StorageError::backend("agentic memory change frame: records must be an array")
726 })?
727 .iter()
728 .map(record_from_frame_json)
729 .collect::<StorageResult<Vec<_>>>()?;
730 validate_unique_record_set(&records, "agentic memory change frame")?;
731 Ok(records)
732}
733
734fn validate_unique_record_set(
735 records: &[AgenticMemoryRecord<JsonValue>],
736 context: &str,
737) -> StorageResult<()> {
738 let mut record_ids = HashSet::new();
739 let mut fragment_ids = HashSet::new();
740 for record in records {
741 if !record_ids.insert(record.id.as_str()) {
742 return Err(StorageError::backend(format!(
743 "{context}: duplicate record id '{}'",
744 record.id
745 )));
746 }
747 if !fragment_ids.insert(record.fragment.id.as_str()) {
748 return Err(StorageError::backend(format!(
749 "{context}: duplicate fragment id '{}'",
750 record.fragment.id
751 )));
752 }
753 }
754 Ok(())
755}
756
757fn record_frame_json(record: &AgenticMemoryRecord<JsonValue>) -> StorageResult<Value> {
758 let codec = agentic_memory_record_frame_codec();
759 let bytes = codec
760 .encode(&agentic_memory_record_frame(record.clone()))
761 .map_err(storage_json_error)?;
762 strict_json_decode(&bytes).map_err(storage_json_error)
763}
764
765fn record_from_frame_json(value: &Value) -> StorageResult<AgenticMemoryRecord<JsonValue>> {
766 strict_canonical_json_bytes(value)
767 .and_then(|bytes| agentic_memory_record_frame_codec().decode(&bytes))
768 .map(|frame: AgenticMemoryRecordFrame| frame.record)
769 .map_err(storage_json_error)
770}
771
772fn cursor_from_json(value: Option<&Value>) -> StorageResult<Option<u64>> {
773 let raw = value
774 .and_then(Value::as_i64)
775 .ok_or_else(|| StorageError::backend("changeCursor must be an integer"))?;
776 if raw < -1 {
777 return Err(StorageError::backend("changeCursor must be >= -1"));
778 }
779 Ok(if raw == -1 { None } else { Some(raw as u64) })
780}
781
782fn cursor_json(change_seq: Option<u64>, snapshot_writes: usize, change_writes: usize) -> Value {
783 json!({
784 "kind": "persistence.cursor",
785 "changeSeq": change_seq,
786 "snapshotWrites": snapshot_writes,
787 "changeWrites": change_writes,
788 })
789}
790
791fn status_json(
792 state: AgenticMemoryRecordsPersistenceStatus,
793 pending: usize,
794 errors: usize,
795 cursor: Value,
796) -> Value {
797 json!({
798 "state": match state {
799 AgenticMemoryRecordsPersistenceStatus::Starting => "starting",
800 AgenticMemoryRecordsPersistenceStatus::Ready => "ready",
801 AgenticMemoryRecordsPersistenceStatus::Flushing => "flushing",
802 AgenticMemoryRecordsPersistenceStatus::Errored => "errored",
803 AgenticMemoryRecordsPersistenceStatus::Disposed => "disposed",
804 },
805 "pending": pending,
806 "writes": cursor.get("snapshotWrites").and_then(Value::as_u64).unwrap_or(0)
807 + cursor.get("changeWrites").and_then(Value::as_u64).unwrap_or(0),
808 "errors": errors,
809 "cursor": cursor,
810 })
811}
812
813fn error_json(phase: &str, err: &StorageError, cursor: Value) -> Value {
814 json!({
815 "phase": phase,
816 "message": err.to_string(),
817 "cursor": cursor,
818 })
819}
820
821fn parse_cursor_fact(value: &Value) -> StorageResult<AgenticMemoryRecordsPersistenceCursor> {
822 Ok(AgenticMemoryRecordsPersistenceCursor {
823 change_seq: value.get("changeSeq").and_then(Value::as_u64),
824 snapshot_writes: value
825 .get("snapshotWrites")
826 .and_then(Value::as_u64)
827 .unwrap_or(0) as usize,
828 change_writes: value
829 .get("changeWrites")
830 .and_then(Value::as_u64)
831 .unwrap_or(0) as usize,
832 })
833}
834
835fn assert_keys<'a>(
836 actual: impl Iterator<Item = &'a str>,
837 expected: impl IntoIterator<Item = &'a str>,
838) -> StorageResult<()> {
839 let mut actual = actual.collect::<Vec<_>>();
840 actual.sort();
841 let mut expected = expected.into_iter().collect::<Vec<_>>();
842 expected.sort();
843 if actual != expected {
844 return Err(StorageError::backend(format!(
845 "unexpected frame fields {}",
846 actual.join(",")
847 )));
848 }
849 Ok(())
850}
851
852fn storage_json_error(error: impl std::fmt::Display) -> StorageError {
853 StorageError::backend(error.to_string())
854}