1use std::collections::{BTreeMap, HashMap};
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
10use std::time::{Duration, Instant};
11
12use serde::{Deserialize, Serialize};
13
14use crate::catalog::StorageLocator;
15use crate::{
16 find_live_runtime, forget_live_runtime, list_live_runtimes, resolve_live_runtime,
17 DiscoveryQuery, FrontendActions, FrontendConnectionState, FrontendRuntimeDescriptor,
18 FrontendTurnState, HarnessCatalog, HttpFrontendRuntime, LiveRuntimeEndpoint,
19 RuntimeAuthorization, RuntimeClientId, RuntimeControllerLease, RuntimeObserverLease,
20 RuntimePermission, SdkError, SdkOperation, Session,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(default)]
26pub struct RuntimeRegistryQuery {
27 pub persisted: DiscoveryQuery,
29 pub include_live: bool,
31 pub include_persisted: bool,
33}
34
35impl Default for RuntimeRegistryQuery {
36 fn default() -> Self {
37 Self {
38 persisted: DiscoveryQuery::default(),
39 include_live: true,
40 include_persisted: true,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum RuntimeRegistryState {
49 Persisted,
51 Idle,
53 Busy,
55 ShuttingDown,
57}
58
59impl RuntimeRegistryState {
60 pub fn as_str(self) -> &'static str {
62 match self {
63 Self::Persisted => "persisted",
64 Self::Idle => "idle",
65 Self::Busy => "busy",
66 Self::ShuttingDown => "shutting_down",
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct RuntimeRegistryOwner {
74 pub pid: u32,
76 pub controller: Option<RuntimeControllerLease>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct RuntimeRegistryEntry {
83 pub id: String,
86 pub runtime_id: Option<String>,
88 pub source_session_id: String,
90 pub source_workspace: Option<PathBuf>,
92 pub source_harness: String,
94 pub profile: Option<String>,
96 pub state: RuntimeRegistryState,
98 pub model: Option<String>,
100 pub owner: Option<RuntimeRegistryOwner>,
102 pub observers: Vec<RuntimeObserverLease>,
104 pub started_at_ms: Option<u128>,
106 pub updated_at_ms: Option<u64>,
108 pub endpoint: Option<LiveRuntimeEndpoint>,
110 pub endpoint_capabilities: Vec<String>,
112 pub actions: Option<FrontendActions>,
114 pub persistence_location: Option<PathBuf>,
116 pub supervisor: Option<crate::LiveRuntimeSupervisor>,
118 pub title: Option<String>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(tag = "kind", rename_all = "snake_case")]
125pub enum RuntimeRegistryEvent {
126 Added {
128 entry: RuntimeRegistryEntry,
130 },
131 Updated {
133 entry: RuntimeRegistryEntry,
135 },
136 Removed {
138 id: String,
140 },
141 Error {
143 message: String,
145 },
146}
147
148pub struct RuntimeRegistryWatch {
150 receiver: tokio::sync::mpsc::Receiver<RuntimeRegistryEvent>,
151 task: tokio::task::JoinHandle<()>,
152}
153
154impl RuntimeRegistryWatch {
155 pub async fn next(&mut self) -> Option<RuntimeRegistryEvent> {
157 self.receiver.recv().await
158 }
159}
160
161impl Drop for RuntimeRegistryWatch {
162 fn drop(&mut self) {
163 self.task.abort();
164 }
165}
166
167#[derive(Debug, Clone, Copy, Default)]
170pub struct LocalRuntimeRegistry;
171
172impl LocalRuntimeRegistry {
173 pub fn new() -> Self {
175 Self
176 }
177
178 pub async fn list(
180 &self,
181 query: &RuntimeRegistryQuery,
182 authorization: &RuntimeAuthorization,
183 ) -> Result<Vec<RuntimeRegistryEntry>, SdkError> {
184 require_permission(authorization, RuntimePermission::Observe)?;
185 let mut entries = BTreeMap::<String, RuntimeRegistryEntry>::new();
186 if query.include_persisted {
187 let persisted = HarnessCatalog::new()
188 .discover(&query.persisted)
189 .map_err(|error| SdkError::Execution {
190 operation: SdkOperation::Discover,
191 message: error.to_string(),
192 })?;
193 for descriptor in persisted {
194 let id = format!(
195 "{}:{}",
196 descriptor.locator.harness.as_str(),
197 descriptor.locator.session_id
198 );
199 let persistence_location = Some(match &descriptor.locator.storage {
200 StorageLocator::File { path } | StorageLocator::Sqlite { path, .. } => {
201 path.clone()
202 }
203 });
204 entries.insert(
205 id.clone(),
206 RuntimeRegistryEntry {
207 id,
208 runtime_id: None,
209 source_session_id: descriptor.locator.session_id,
210 source_workspace: None,
211 source_harness: descriptor.locator.harness.0,
212 profile: None,
213 state: RuntimeRegistryState::Persisted,
214 model: descriptor.model,
215 owner: None,
216 observers: Vec::new(),
217 started_at_ms: None,
218 updated_at_ms: descriptor.updated_at_ms,
219 endpoint: None,
220 endpoint_capabilities: Vec::new(),
221 actions: None,
222 persistence_location,
223 supervisor: None,
224 title: descriptor.title,
225 },
226 );
227 }
228 }
229 if query.include_live {
230 for record in list_live_runtimes().map_err(registry_receipt_error)? {
231 let Some((probe, descriptor)) = probe_receipt(&record).await? else {
232 continue;
233 };
234 let leases = probe.lease_snapshot().await?;
235 let entry = live_entry(record, descriptor, leases);
236 if entries.insert(entry.id.clone(), entry).is_some() {
237 return Err(SdkError::Execution {
238 operation: SdkOperation::Discover,
239 message: "duplicate stable runtime id in live registry".into(),
240 });
241 }
242 }
243 }
244 Ok(entries.into_values().collect())
245 }
246
247 pub async fn describe(
249 &self,
250 id: &str,
251 query: &RuntimeRegistryQuery,
252 authorization: &RuntimeAuthorization,
253 ) -> Result<RuntimeRegistryEntry, SdkError> {
254 self.list(query, authorization)
255 .await?
256 .into_iter()
257 .find(|entry| entry.id == id)
258 .ok_or_else(|| SdkError::NotFound {
259 operation: SdkOperation::Discover,
260 message: format!("runtime or persisted session `{id}`"),
261 })
262 }
263
264 pub async fn source_state(
273 &self,
274 harness: &str,
275 session_id: &str,
276 authorization: &RuntimeAuthorization,
277 ) -> Result<Option<RuntimeRegistryState>, SdkError> {
278 require_permission(authorization, RuntimePermission::Observe)?;
279 for record in list_live_runtimes().map_err(registry_receipt_error)? {
280 if record.source.harness != harness || record.source.session_id != session_id {
281 continue;
282 }
283 let Some((_probe, descriptor)) = probe_receipt(&record).await? else {
284 continue;
285 };
286 return Ok(Some(reconciled_state(&descriptor)));
287 }
288 Ok(None)
289 }
290
291 pub async fn attach(
293 &self,
294 runtime_id: &str,
295 client_id: RuntimeClientId,
296 authorization: RuntimeAuthorization,
297 ) -> Result<Arc<HttpFrontendRuntime>, SdkError> {
298 require_permission(&authorization, RuntimePermission::Observe)?;
299 let record = find_live_runtime(runtime_id)
300 .map_err(registry_receipt_error)?
301 .ok_or_else(|| SdkError::NotFound {
302 operation: SdkOperation::Resume,
303 message: format!("live runtime `{runtime_id}`"),
304 })?;
305 let resolved = resolve_live_runtime(&record.endpoint, &record.source)
306 .map_err(registry_receipt_error)?;
307 let attached = HttpFrontendRuntime::connect_with_authorization(
308 resolved.base_url,
309 resolved.token,
310 client_id,
311 authorization,
312 )
313 .await?;
314 note_reachable(&record.endpoint);
318 Ok(attached)
319 }
320
321 pub fn load_persisted(
323 &self,
324 id: &str,
325 query: &RuntimeRegistryQuery,
326 authorization: &RuntimeAuthorization,
327 ) -> Result<Session, SdkError> {
328 require_permission(authorization, RuntimePermission::Observe)?;
329 let descriptor = HarnessCatalog::new()
330 .discover(&query.persisted)
331 .map_err(|error| SdkError::Execution {
332 operation: SdkOperation::Discover,
333 message: error.to_string(),
334 })?
335 .into_iter()
336 .find(|descriptor| {
337 format!(
338 "{}:{}",
339 descriptor.locator.harness.as_str(),
340 descriptor.locator.session_id
341 ) == id
342 })
343 .ok_or_else(|| SdkError::NotFound {
344 operation: SdkOperation::Load,
345 message: format!("persisted session `{id}`"),
346 })?;
347 HarnessCatalog::new()
348 .load(&descriptor.locator)
349 .map_err(|error| SdkError::Execution {
350 operation: SdkOperation::Load,
351 message: error.to_string(),
352 })
353 }
354
355 pub fn watch(
357 &self,
358 query: RuntimeRegistryQuery,
359 authorization: RuntimeAuthorization,
360 poll_interval: Duration,
361 ) -> Result<RuntimeRegistryWatch, SdkError> {
362 require_permission(&authorization, RuntimePermission::Observe)?;
363 let (sender, receiver) = tokio::sync::mpsc::channel(128);
364 let registry = *self;
365 let interval = poll_interval.max(Duration::from_millis(25));
366 let task = tokio::spawn(async move {
367 let mut previous = BTreeMap::<String, RuntimeRegistryEntry>::new();
368 let mut ticker = tokio::time::interval(interval);
369 loop {
370 ticker.tick().await;
371 let current = match registry.list(&query, &authorization).await {
372 Ok(entries) => entries
373 .into_iter()
374 .map(|entry| (entry.id.clone(), entry))
375 .collect::<BTreeMap<_, _>>(),
376 Err(error) => {
377 if sender
378 .send(RuntimeRegistryEvent::Error {
379 message: error.to_string(),
380 })
381 .await
382 .is_err()
383 {
384 return;
385 }
386 continue;
387 }
388 };
389 for (id, entry) in ¤t {
390 let event = match previous.get(id) {
391 None => Some(RuntimeRegistryEvent::Added {
392 entry: entry.clone(),
393 }),
394 Some(prior) if prior != entry => Some(RuntimeRegistryEvent::Updated {
395 entry: entry.clone(),
396 }),
397 Some(_) => None,
398 };
399 if let Some(event) = event {
400 if sender.send(event).await.is_err() {
401 return;
402 }
403 }
404 }
405 for id in previous.keys().filter(|id| !current.contains_key(*id)) {
406 if sender
407 .send(RuntimeRegistryEvent::Removed { id: id.clone() })
408 .await
409 .is_err()
410 {
411 return;
412 }
413 }
414 previous = current;
415 }
416 });
417 Ok(RuntimeRegistryWatch { receiver, task })
418 }
419}
420
421const FORGET_AFTER_FAILED_PROBES: u32 = 3;
423const FORGET_AFTER_UNREACHABLE_FOR: Duration = Duration::from_secs(2);
429
430async fn probe_receipt(
468 record: &crate::LiveRuntimeRecord,
469) -> Result<Option<(Arc<HttpFrontendRuntime>, FrontendRuntimeDescriptor)>, SdkError> {
470 let Ok(resolved) = resolve_live_runtime(&record.endpoint, &record.source) else {
471 note_unreachable(&record.endpoint);
472 return Ok(None);
473 };
474 let probe_id = registry_probe_id(&record.endpoint)?;
475 match HttpFrontendRuntime::probe_described(resolved.base_url, resolved.token, probe_id).await {
476 Ok(probed) => {
477 note_reachable(&record.endpoint);
478 Ok(Some(probed))
479 }
480 Err(_) => {
484 note_unreachable(&record.endpoint);
485 Ok(None)
486 }
487 }
488}
489
490struct Outage {
493 started: Instant,
494 latest: Instant,
495 failures: u32,
496}
497
498fn outages() -> &'static Mutex<HashMap<String, Outage>> {
502 static OUTAGES: OnceLock<Mutex<HashMap<String, Outage>>> = OnceLock::new();
503 OUTAGES.get_or_init(|| Mutex::new(HashMap::new()))
504}
505
506fn lock_outages() -> MutexGuard<'static, HashMap<String, Outage>> {
507 outages()
508 .lock()
509 .unwrap_or_else(std::sync::PoisonError::into_inner)
510}
511
512fn note_reachable(endpoint: &LiveRuntimeEndpoint) {
517 lock_outages().remove(endpoint.as_str());
518}
519
520fn note_unreachable(endpoint: &LiveRuntimeEndpoint) {
523 let now = Instant::now();
524 let corroborated = {
525 let mut outages = lock_outages();
526 outages
532 .retain(|_, outage| now.duration_since(outage.latest) <= FORGET_AFTER_UNREACHABLE_FOR);
533 let outage = outages
534 .entry(endpoint.as_str().to_string())
535 .or_insert(Outage {
536 started: now,
537 latest: now,
538 failures: 0,
539 });
540 outage.failures += 1;
541 outage.latest = now;
542 let corroborated = outage.failures >= FORGET_AFTER_FAILED_PROBES
543 && now.duration_since(outage.started) >= FORGET_AFTER_UNREACHABLE_FOR;
544 if corroborated {
545 outages.remove(endpoint.as_str());
546 }
547 corroborated
548 };
549 if corroborated {
552 let _ = forget_live_runtime(endpoint);
553 }
554}
555
556fn reconciled_state(descriptor: &FrontendRuntimeDescriptor) -> RuntimeRegistryState {
560 if descriptor.connection_state == FrontendConnectionState::ShuttingDown {
561 RuntimeRegistryState::ShuttingDown
562 } else if descriptor.turn_state == FrontendTurnState::Busy {
563 RuntimeRegistryState::Busy
564 } else {
565 RuntimeRegistryState::Idle
566 }
567}
568
569fn registry_probe_id(endpoint: &LiveRuntimeEndpoint) -> Result<RuntimeClientId, SdkError> {
570 RuntimeClientId::parse(format!(
571 "registry-{}",
572 endpoint.as_str().rsplit('/').next().unwrap_or("probe")
573 ))
574 .map_err(|error| SdkError::InvalidArgument {
575 operation: SdkOperation::Discover,
576 message: error.to_string(),
577 })
578}
579
580fn live_entry(
581 record: crate::LiveRuntimeRecord,
582 descriptor: FrontendRuntimeDescriptor,
583 leases: crate::RuntimeLeaseSnapshot,
584) -> RuntimeRegistryEntry {
585 let state = reconciled_state(&descriptor);
586 RuntimeRegistryEntry {
587 id: record.runtime_session_id.clone(),
588 runtime_id: Some(record.runtime_session_id),
589 source_session_id: record.source.session_id,
590 source_workspace: Some(record.source.workspace),
591 source_harness: record.source.harness,
592 profile: descriptor
593 .emulation_profile
594 .or(record.metadata.profile.clone()),
595 state,
596 model: Some(descriptor.model),
597 owner: Some(RuntimeRegistryOwner {
598 pid: record.pid,
599 controller: leases.controller,
600 }),
601 observers: leases.observers,
602 started_at_ms: Some(record.created_at_ms),
603 updated_at_ms: None,
604 endpoint: Some(record.endpoint),
605 endpoint_capabilities: record.metadata.endpoint_capabilities,
606 actions: Some(descriptor.actions),
607 persistence_location: record.metadata.persistence_location,
608 supervisor: record.metadata.supervisor,
609 title: None,
610 }
611}
612
613fn require_permission(
614 authorization: &RuntimeAuthorization,
615 permission: RuntimePermission,
616) -> Result<(), SdkError> {
617 if authorization.allows(permission) {
618 Ok(())
619 } else {
620 Err(SdkError::Unauthorized {
621 permission: permission.as_str().into(),
622 })
623 }
624}
625
626fn registry_receipt_error(error: crate::LiveRuntimeReceiptError) -> SdkError {
627 SdkError::Execution {
628 operation: SdkOperation::Discover,
629 message: error.to_string(),
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use crate::server::{run_http, RpcEngine};
637 use crate::{
638 register_live_runtime_with_metadata, Agent, ChatMessage, ChatRequest, Config, HarnessHomes,
639 HarnessId, LiveRuntimeMetadata, LiveRuntimeSource, Provider, SdkRuntime, Usage,
640 };
641 use async_trait::async_trait;
642
643 struct SaysProvider;
644
645 #[async_trait]
646 impl Provider for SaysProvider {
647 async fn complete(
648 &self,
649 _request: &ChatRequest,
650 _on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
651 ) -> crate::Result<(ChatMessage, Usage)> {
652 Ok((ChatMessage::assistant("registry reply"), Usage::default()))
653 }
654 }
655
656 fn root(label: &str) -> PathBuf {
657 let nonce = std::time::SystemTime::now()
658 .duration_since(std::time::UNIX_EPOCH)
659 .unwrap()
660 .as_nanos();
661 let path = std::env::temp_dir().join(format!(
662 "supercode-runtime-registry-{label}-{}-{}",
663 std::process::id(),
664 nonce
665 ));
666 std::fs::create_dir_all(&path).unwrap();
667 path
668 }
669
670 #[tokio::test]
671 #[allow(clippy::await_holding_lock)]
672 async fn joined_registry_lists_watches_attaches_and_reconciles_without_data_loss() {
673 let _guard = crate::live_runtime::test_environment_lock();
674 let home = root("live");
675 let workspace = home.join("workspace");
676 std::fs::create_dir_all(&workspace).unwrap();
677 let persisted = home.join("canonical.jsonl");
678 std::fs::write(&persisted, "SOURCE_BYTES_MUST_SURVIVE\n").unwrap();
679 std::env::set_var("SUPERCODE_HOME", &home);
680
681 let agent = Agent::with_provider(
682 Config::builder().cwd(workspace.clone()).build(),
683 Box::new(SaysProvider),
684 );
685 let engine = RpcEngine::new_named(agent, "live-registry-1", None);
686 let token: Arc<str> = "registry-owner-token".into();
687 let address = run_http(engine.clone(), "127.0.0.1:0", token.clone())
688 .await
689 .unwrap();
690 let registry = LocalRuntimeRegistry::new();
691 let query = RuntimeRegistryQuery {
692 include_live: true,
693 include_persisted: false,
694 ..RuntimeRegistryQuery::default()
695 };
696 let mut watch = registry
697 .watch(
698 query.clone(),
699 RuntimeAuthorization::observer(),
700 Duration::from_millis(25),
701 )
702 .unwrap();
703 let registration = register_live_runtime_with_metadata(
704 "live-registry-1",
705 LiveRuntimeSource {
706 harness: "claude-code".into(),
707 session_id: "source-1".into(),
708 workspace: workspace.clone(),
709 },
710 format!("http://{address}"),
711 token.to_string(),
712 LiveRuntimeMetadata {
713 profile: Some("cc-parity".into()),
714 persistence_location: Some(persisted.clone()),
715 endpoint_capabilities: vec!["http".into(), "acp".into()],
716 supervisor: None,
717 },
718 )
719 .unwrap();
720
721 let added = tokio::time::timeout(Duration::from_secs(2), watch.next())
722 .await
723 .unwrap()
724 .unwrap();
725 assert!(matches!(
726 added,
727 RuntimeRegistryEvent::Added { ref entry }
728 if entry.id == "live-registry-1"
729 && entry.profile.as_deref() == Some("cc-parity")
730 && entry.state == RuntimeRegistryState::Idle
731 && entry.persistence_location.as_ref() == Some(&persisted)
732 && entry.owner.as_ref().unwrap().pid == std::process::id()
733 && entry.observers.is_empty()
734 && !entry.actions.as_ref().unwrap().submit
735 ));
736
737 let observer = registry
738 .attach(
739 "live-registry-1",
740 RuntimeClientId::parse("registry-observer").unwrap(),
741 RuntimeAuthorization::observer(),
742 )
743 .await
744 .unwrap();
745 assert!(!observer.describe().await.unwrap().actions.submit);
746 assert!(matches!(
747 observer.submit("denied".into()).await,
748 Err(SdkError::Unauthorized { ref permission }) if permission == "interact"
749 ));
750 let owner = registry
751 .attach(
752 "live-registry-1",
753 RuntimeClientId::parse("registry-owner").unwrap(),
754 RuntimeAuthorization::owner(),
755 )
756 .await
757 .unwrap();
758 assert_eq!(
759 owner.submit("continue".into()).await.unwrap(),
760 "registry reply"
761 );
762 let listed = registry
763 .list(&query, &RuntimeAuthorization::owner())
764 .await
765 .unwrap();
766 assert_eq!(listed.len(), 1);
767 assert_eq!(listed[0].observers.len(), 2);
768 assert_eq!(
769 listed[0]
770 .owner
771 .as_ref()
772 .and_then(|owner| owner.controller.as_ref())
773 .map(|lease| lease.client_id.as_str()),
774 Some("registry-owner")
775 );
776
777 owner.close().await.unwrap();
778 engine.wait_for_shutdown().await;
779 drop(registration);
780 let removed = tokio::time::timeout(Duration::from_secs(2), async {
781 loop {
782 let event = watch.next().await.unwrap();
783 if matches!(event, RuntimeRegistryEvent::Removed { .. }) {
784 break event;
785 }
786 }
787 })
788 .await
789 .unwrap();
790 assert_eq!(
791 removed,
792 RuntimeRegistryEvent::Removed {
793 id: "live-registry-1".into()
794 }
795 );
796 assert_eq!(
797 std::fs::read_to_string(&persisted).unwrap(),
798 "SOURCE_BYTES_MUST_SURVIVE\n"
799 );
800 std::env::remove_var("SUPERCODE_HOME");
801 std::fs::remove_dir_all(home).ok();
802 }
803
804 #[test]
805 fn persisted_registry_entries_load_through_the_canonical_catalog() {
806 let root = root("persisted");
807 let workspace = root.join("workspace");
808 let claude = root.join("claude");
809 std::fs::create_dir_all(&workspace).unwrap();
810 std::fs::create_dir_all(&claude).unwrap();
811 let session_path = claude.join("session.jsonl");
812 std::fs::write(
813 &session_path,
814 format!(
815 "{{\"type\":\"user\",\"sessionId\":\"cc-registry\",\"cwd\":{},\"message\":{{\"role\":\"user\",\"content\":\"persisted fact\"}}}}\n",
816 serde_json::to_string(&workspace.to_string_lossy()).unwrap()
817 ),
818 )
819 .unwrap();
820 let empty = root.join("empty");
821 std::fs::create_dir_all(&empty).unwrap();
822 let query = RuntimeRegistryQuery {
823 persisted: DiscoveryQuery {
824 workspace: Some(workspace),
825 harnesses: vec![HarnessId::from(HarnessId::CLAUDE_CODE)],
826 homes: HarnessHomes {
827 claude_code: claude,
828 codex: empty.clone(),
829 pi: empty.clone(),
830 opencode: empty.clone(),
831 grok: empty.clone(),
832 gemini: empty.clone(),
833 goose: empty.clone(),
834 supercode: empty,
835 },
836 cursor: None,
837 limit: None,
838 query: None,
839 include_topic_candidates: false,
840 include_child_sessions: false,
841 root_session_id: None,
842 },
843 include_live: false,
844 include_persisted: true,
845 };
846 let registry = LocalRuntimeRegistry::new();
847 let entries =
848 futures::executor::block_on(registry.list(&query, &RuntimeAuthorization::observer()))
849 .unwrap();
850 assert_eq!(entries.len(), 1);
851 assert_eq!(entries[0].id, "claude-code:cc-registry");
852 assert_eq!(entries[0].state, RuntimeRegistryState::Persisted);
853 assert_eq!(
854 entries[0].persistence_location.as_ref(),
855 Some(&session_path)
856 );
857 let loaded = registry
858 .load_persisted(
859 "claude-code:cc-registry",
860 &query,
861 &RuntimeAuthorization::observer(),
862 )
863 .unwrap();
864 assert_eq!(loaded.messages.len(), 1);
865 assert_eq!(
866 loaded.messages[0].content.as_deref(),
867 Some("persisted fact")
868 );
869 std::fs::remove_dir_all(root).ok();
870 }
871}