1use std::collections::{BTreeMap, BTreeSet, 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(crate) async fn source_states(
295 &self,
296 sources: &BTreeSet<(String, String)>,
297 authorization: &RuntimeAuthorization,
298 ) -> Result<BTreeMap<(String, String), RuntimeRegistryState>, SdkError> {
299 require_permission(authorization, RuntimePermission::Observe)?;
300 if sources.is_empty() {
301 return Ok(BTreeMap::new());
302 }
303 let mut runtimes = BTreeMap::new();
306 for record in list_live_runtimes().map_err(registry_receipt_error)? {
307 let key = (
308 record.source.harness.clone(),
309 record.source.session_id.clone(),
310 );
311 if !sources.contains(&key) {
312 continue;
313 }
314 let Some((_probe, descriptor)) = probe_receipt(&record).await? else {
315 continue;
316 };
317 if runtimes
318 .insert(
319 record.runtime_session_id,
320 (key, reconciled_state(&descriptor)),
321 )
322 .is_some()
323 {
324 return Err(SdkError::Execution {
325 operation: SdkOperation::Discover,
326 message: "duplicate stable runtime id in live registry".into(),
327 });
328 }
329 }
330 Ok(runtimes.into_values().collect())
331 }
332
333 pub async fn attach(
335 &self,
336 runtime_id: &str,
337 client_id: RuntimeClientId,
338 authorization: RuntimeAuthorization,
339 ) -> Result<Arc<HttpFrontendRuntime>, SdkError> {
340 require_permission(&authorization, RuntimePermission::Observe)?;
341 let record = find_live_runtime(runtime_id)
342 .map_err(registry_receipt_error)?
343 .ok_or_else(|| SdkError::NotFound {
344 operation: SdkOperation::Resume,
345 message: format!("live runtime `{runtime_id}`"),
346 })?;
347 let resolved = resolve_live_runtime(&record.endpoint, &record.source)
348 .map_err(registry_receipt_error)?;
349 let attached = HttpFrontendRuntime::connect_with_authorization(
350 resolved.base_url,
351 resolved.token,
352 client_id,
353 authorization,
354 )
355 .await?;
356 note_reachable(&record.endpoint);
360 Ok(attached)
361 }
362
363 pub fn load_persisted(
365 &self,
366 id: &str,
367 query: &RuntimeRegistryQuery,
368 authorization: &RuntimeAuthorization,
369 ) -> Result<Session, SdkError> {
370 require_permission(authorization, RuntimePermission::Observe)?;
371 let descriptor = HarnessCatalog::new()
372 .discover(&query.persisted)
373 .map_err(|error| SdkError::Execution {
374 operation: SdkOperation::Discover,
375 message: error.to_string(),
376 })?
377 .into_iter()
378 .find(|descriptor| {
379 format!(
380 "{}:{}",
381 descriptor.locator.harness.as_str(),
382 descriptor.locator.session_id
383 ) == id
384 })
385 .ok_or_else(|| SdkError::NotFound {
386 operation: SdkOperation::Load,
387 message: format!("persisted session `{id}`"),
388 })?;
389 HarnessCatalog::new()
390 .load(&descriptor.locator)
391 .map_err(|error| SdkError::Execution {
392 operation: SdkOperation::Load,
393 message: error.to_string(),
394 })
395 }
396
397 pub fn watch(
399 &self,
400 query: RuntimeRegistryQuery,
401 authorization: RuntimeAuthorization,
402 poll_interval: Duration,
403 ) -> Result<RuntimeRegistryWatch, SdkError> {
404 require_permission(&authorization, RuntimePermission::Observe)?;
405 let (sender, receiver) = tokio::sync::mpsc::channel(128);
406 let registry = *self;
407 let interval = poll_interval.max(Duration::from_millis(25));
408 let task = tokio::spawn(async move {
409 let mut previous = BTreeMap::<String, RuntimeRegistryEntry>::new();
410 let mut ticker = tokio::time::interval(interval);
411 loop {
412 ticker.tick().await;
413 let current = match registry.list(&query, &authorization).await {
414 Ok(entries) => entries
415 .into_iter()
416 .map(|entry| (entry.id.clone(), entry))
417 .collect::<BTreeMap<_, _>>(),
418 Err(error) => {
419 if sender
420 .send(RuntimeRegistryEvent::Error {
421 message: error.to_string(),
422 })
423 .await
424 .is_err()
425 {
426 return;
427 }
428 continue;
429 }
430 };
431 for (id, entry) in ¤t {
432 let event = match previous.get(id) {
433 None => Some(RuntimeRegistryEvent::Added {
434 entry: entry.clone(),
435 }),
436 Some(prior) if prior != entry => Some(RuntimeRegistryEvent::Updated {
437 entry: entry.clone(),
438 }),
439 Some(_) => None,
440 };
441 if let Some(event) = event {
442 if sender.send(event).await.is_err() {
443 return;
444 }
445 }
446 }
447 for id in previous.keys().filter(|id| !current.contains_key(*id)) {
448 if sender
449 .send(RuntimeRegistryEvent::Removed { id: id.clone() })
450 .await
451 .is_err()
452 {
453 return;
454 }
455 }
456 previous = current;
457 }
458 });
459 Ok(RuntimeRegistryWatch { receiver, task })
460 }
461}
462
463const FORGET_AFTER_FAILED_PROBES: u32 = 3;
465const FORGET_AFTER_UNREACHABLE_FOR: Duration = Duration::from_secs(2);
471
472async fn probe_receipt(
510 record: &crate::LiveRuntimeRecord,
511) -> Result<Option<(Arc<HttpFrontendRuntime>, FrontendRuntimeDescriptor)>, SdkError> {
512 let Ok(resolved) = resolve_live_runtime(&record.endpoint, &record.source) else {
513 note_unreachable(&record.endpoint);
514 return Ok(None);
515 };
516 let probe_id = registry_probe_id(&record.endpoint)?;
517 match HttpFrontendRuntime::probe_described(resolved.base_url, resolved.token, probe_id).await {
518 Ok(probed) => {
519 note_reachable(&record.endpoint);
520 Ok(Some(probed))
521 }
522 Err(_) => {
526 note_unreachable(&record.endpoint);
527 Ok(None)
528 }
529 }
530}
531
532struct Outage {
535 started: Instant,
536 latest: Instant,
537 failures: u32,
538}
539
540fn outages() -> &'static Mutex<HashMap<String, Outage>> {
544 static OUTAGES: OnceLock<Mutex<HashMap<String, Outage>>> = OnceLock::new();
545 OUTAGES.get_or_init(|| Mutex::new(HashMap::new()))
546}
547
548fn lock_outages() -> MutexGuard<'static, HashMap<String, Outage>> {
549 outages()
550 .lock()
551 .unwrap_or_else(std::sync::PoisonError::into_inner)
552}
553
554fn note_reachable(endpoint: &LiveRuntimeEndpoint) {
559 lock_outages().remove(endpoint.as_str());
560}
561
562fn note_unreachable(endpoint: &LiveRuntimeEndpoint) {
565 let now = Instant::now();
566 let corroborated = {
567 let mut outages = lock_outages();
568 outages
574 .retain(|_, outage| now.duration_since(outage.latest) <= FORGET_AFTER_UNREACHABLE_FOR);
575 let outage = outages
576 .entry(endpoint.as_str().to_string())
577 .or_insert(Outage {
578 started: now,
579 latest: now,
580 failures: 0,
581 });
582 outage.failures += 1;
583 outage.latest = now;
584 let corroborated = outage.failures >= FORGET_AFTER_FAILED_PROBES
585 && now.duration_since(outage.started) >= FORGET_AFTER_UNREACHABLE_FOR;
586 if corroborated {
587 outages.remove(endpoint.as_str());
588 }
589 corroborated
590 };
591 if corroborated {
594 let _ = forget_live_runtime(endpoint);
595 }
596}
597
598fn reconciled_state(descriptor: &FrontendRuntimeDescriptor) -> RuntimeRegistryState {
602 if descriptor.connection_state == FrontendConnectionState::ShuttingDown {
603 RuntimeRegistryState::ShuttingDown
604 } else if descriptor.turn_state == FrontendTurnState::Busy {
605 RuntimeRegistryState::Busy
606 } else {
607 RuntimeRegistryState::Idle
608 }
609}
610
611fn registry_probe_id(endpoint: &LiveRuntimeEndpoint) -> Result<RuntimeClientId, SdkError> {
612 RuntimeClientId::parse(format!(
613 "registry-{}",
614 endpoint.as_str().rsplit('/').next().unwrap_or("probe")
615 ))
616 .map_err(|error| SdkError::InvalidArgument {
617 operation: SdkOperation::Discover,
618 message: error.to_string(),
619 })
620}
621
622fn live_entry(
623 record: crate::LiveRuntimeRecord,
624 descriptor: FrontendRuntimeDescriptor,
625 leases: crate::RuntimeLeaseSnapshot,
626) -> RuntimeRegistryEntry {
627 let state = reconciled_state(&descriptor);
628 RuntimeRegistryEntry {
629 id: record.runtime_session_id.clone(),
630 runtime_id: Some(record.runtime_session_id),
631 source_session_id: record.source.session_id,
632 source_workspace: Some(record.source.workspace),
633 source_harness: record.source.harness,
634 profile: descriptor
635 .emulation_profile
636 .or(record.metadata.profile.clone()),
637 state,
638 model: Some(descriptor.model),
639 owner: Some(RuntimeRegistryOwner {
640 pid: record.pid,
641 controller: leases.controller,
642 }),
643 observers: leases.observers,
644 started_at_ms: Some(record.created_at_ms),
645 updated_at_ms: None,
646 endpoint: Some(record.endpoint),
647 endpoint_capabilities: record.metadata.endpoint_capabilities,
648 actions: Some(descriptor.actions),
649 persistence_location: record.metadata.persistence_location,
650 supervisor: record.metadata.supervisor,
651 title: None,
652 }
653}
654
655fn require_permission(
656 authorization: &RuntimeAuthorization,
657 permission: RuntimePermission,
658) -> Result<(), SdkError> {
659 if authorization.allows(permission) {
660 Ok(())
661 } else {
662 Err(SdkError::Unauthorized {
663 permission: permission.as_str().into(),
664 })
665 }
666}
667
668fn registry_receipt_error(error: crate::LiveRuntimeReceiptError) -> SdkError {
669 SdkError::Execution {
670 operation: SdkOperation::Discover,
671 message: error.to_string(),
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 use crate::server::{run_http, RpcEngine};
679 use crate::{
680 register_live_runtime_with_metadata, Agent, ChatMessage, ChatRequest, Config, HarnessHomes,
681 HarnessId, LiveRuntimeMetadata, LiveRuntimeSource, Provider, SdkRuntime, Usage,
682 };
683 use async_trait::async_trait;
684
685 struct SaysProvider;
686
687 #[async_trait]
688 impl Provider for SaysProvider {
689 async fn complete(
690 &self,
691 _request: &ChatRequest,
692 _on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
693 ) -> crate::Result<(ChatMessage, Usage)> {
694 Ok((ChatMessage::assistant("registry reply"), Usage::default()))
695 }
696 }
697
698 fn root(label: &str) -> PathBuf {
699 let nonce = std::time::SystemTime::now()
700 .duration_since(std::time::UNIX_EPOCH)
701 .unwrap()
702 .as_nanos();
703 let path = std::env::temp_dir().join(format!(
704 "supercode-runtime-registry-{label}-{}-{}",
705 std::process::id(),
706 nonce
707 ));
708 std::fs::create_dir_all(&path).unwrap();
709 path
710 }
711
712 #[tokio::test]
713 #[allow(clippy::await_holding_lock)]
714 async fn joined_registry_lists_watches_attaches_and_reconciles_without_data_loss() {
715 let _guard = crate::live_runtime::test_environment_lock();
716 let home = root("live");
717 let workspace = home.join("workspace");
718 std::fs::create_dir_all(&workspace).unwrap();
719 let persisted = home.join("canonical.jsonl");
720 std::fs::write(&persisted, "SOURCE_BYTES_MUST_SURVIVE\n").unwrap();
721 std::env::set_var("SUPERCODE_HOME", &home);
722
723 let agent = Agent::with_provider(
724 Config::builder().cwd(workspace.clone()).build(),
725 Box::new(SaysProvider),
726 );
727 let engine = RpcEngine::new_named(agent, "live-registry-1", None);
728 let token: Arc<str> = "registry-owner-token".into();
729 let address = run_http(engine.clone(), "127.0.0.1:0", token.clone())
730 .await
731 .unwrap();
732 let registry = LocalRuntimeRegistry::new();
733 let query = RuntimeRegistryQuery {
734 include_live: true,
735 include_persisted: false,
736 ..RuntimeRegistryQuery::default()
737 };
738 let mut watch = registry
739 .watch(
740 query.clone(),
741 RuntimeAuthorization::observer(),
742 Duration::from_millis(25),
743 )
744 .unwrap();
745 let registration = register_live_runtime_with_metadata(
746 "live-registry-1",
747 LiveRuntimeSource {
748 harness: "claude-code".into(),
749 session_id: "source-1".into(),
750 workspace: workspace.clone(),
751 },
752 format!("http://{address}"),
753 token.to_string(),
754 LiveRuntimeMetadata {
755 profile: Some("cc-parity".into()),
756 persistence_location: Some(persisted.clone()),
757 endpoint_capabilities: vec!["http".into(), "acp".into()],
758 supervisor: None,
759 },
760 )
761 .unwrap();
762
763 let added = tokio::time::timeout(Duration::from_secs(2), watch.next())
764 .await
765 .unwrap()
766 .unwrap();
767 assert!(matches!(
768 added,
769 RuntimeRegistryEvent::Added { ref entry }
770 if entry.id == "live-registry-1"
771 && entry.profile.as_deref() == Some("cc-parity")
772 && entry.state == RuntimeRegistryState::Idle
773 && entry.persistence_location.as_ref() == Some(&persisted)
774 && entry.owner.as_ref().unwrap().pid == std::process::id()
775 && entry.observers.is_empty()
776 && !entry.actions.as_ref().unwrap().submit
777 ));
778
779 let observer = registry
780 .attach(
781 "live-registry-1",
782 RuntimeClientId::parse("registry-observer").unwrap(),
783 RuntimeAuthorization::observer(),
784 )
785 .await
786 .unwrap();
787 assert!(!observer.describe().await.unwrap().actions.submit);
788 assert!(matches!(
789 observer.submit("denied".into()).await,
790 Err(SdkError::Unauthorized { ref permission }) if permission == "interact"
791 ));
792 let owner = registry
793 .attach(
794 "live-registry-1",
795 RuntimeClientId::parse("registry-owner").unwrap(),
796 RuntimeAuthorization::owner(),
797 )
798 .await
799 .unwrap();
800 assert_eq!(
801 owner.submit("continue".into()).await.unwrap(),
802 "registry reply"
803 );
804 let listed = registry
805 .list(&query, &RuntimeAuthorization::owner())
806 .await
807 .unwrap();
808 assert_eq!(listed.len(), 1);
809 assert_eq!(listed[0].observers.len(), 2);
810 assert_eq!(
811 listed[0]
812 .owner
813 .as_ref()
814 .and_then(|owner| owner.controller.as_ref())
815 .map(|lease| lease.client_id.as_str()),
816 Some("registry-owner")
817 );
818
819 owner.close().await.unwrap();
820 engine.wait_for_shutdown().await;
821 drop(registration);
822 let removed = tokio::time::timeout(Duration::from_secs(2), async {
823 loop {
824 let event = watch.next().await.unwrap();
825 if matches!(event, RuntimeRegistryEvent::Removed { .. }) {
826 break event;
827 }
828 }
829 })
830 .await
831 .unwrap();
832 assert_eq!(
833 removed,
834 RuntimeRegistryEvent::Removed {
835 id: "live-registry-1".into()
836 }
837 );
838 assert_eq!(
839 std::fs::read_to_string(&persisted).unwrap(),
840 "SOURCE_BYTES_MUST_SURVIVE\n"
841 );
842 std::env::remove_var("SUPERCODE_HOME");
843 std::fs::remove_dir_all(home).ok();
844 }
845
846 #[test]
847 fn persisted_registry_entries_load_through_the_canonical_catalog() {
848 let root = root("persisted");
849 let workspace = root.join("workspace");
850 let claude = root.join("claude");
851 std::fs::create_dir_all(&workspace).unwrap();
852 std::fs::create_dir_all(&claude).unwrap();
853 let session_path = claude.join("session.jsonl");
854 std::fs::write(
855 &session_path,
856 format!(
857 "{{\"type\":\"user\",\"sessionId\":\"cc-registry\",\"cwd\":{},\"message\":{{\"role\":\"user\",\"content\":\"persisted fact\"}}}}\n",
858 serde_json::to_string(&workspace.to_string_lossy()).unwrap()
859 ),
860 )
861 .unwrap();
862 let empty = root.join("empty");
863 std::fs::create_dir_all(&empty).unwrap();
864 let query = RuntimeRegistryQuery {
865 persisted: DiscoveryQuery {
866 workspace: Some(workspace),
867 harnesses: vec![HarnessId::from(HarnessId::CLAUDE_CODE)],
868 homes: HarnessHomes {
869 claude_code: claude,
870 codex: empty.clone(),
871 pi: empty.clone(),
872 opencode: empty.clone(),
873 grok: empty.clone(),
874 gemini: empty.clone(),
875 goose: empty.clone(),
876 supercode: empty.clone(),
877 openclaw: empty.clone(),
878 hermes: empty.clone(),
879 orchestrator: empty,
880 },
881 cursor: None,
882 limit: None,
883 query: None,
884 search_previews: false,
885 include_topic_candidates: false,
886 include_child_sessions: false,
887 root_session_id: None,
888 profile: None,
889 workspace_family: None,
890 updated_after_ms: None,
891 updated_before_ms: None,
892 },
893 include_live: false,
894 include_persisted: true,
895 };
896 let registry = LocalRuntimeRegistry::new();
897 let entries =
898 futures::executor::block_on(registry.list(&query, &RuntimeAuthorization::observer()))
899 .unwrap();
900 assert_eq!(entries.len(), 1);
901 assert_eq!(entries[0].id, "claude-code:cc-registry");
902 assert_eq!(entries[0].state, RuntimeRegistryState::Persisted);
903 assert_eq!(
904 entries[0].persistence_location.as_ref(),
905 Some(&session_path)
906 );
907 let loaded = registry
908 .load_persisted(
909 "claude-code:cc-registry",
910 &query,
911 &RuntimeAuthorization::observer(),
912 )
913 .unwrap();
914 assert_eq!(loaded.messages.len(), 1);
915 assert_eq!(
916 loaded.messages[0].content.as_deref(),
917 Some("persisted fact")
918 );
919 std::fs::remove_dir_all(root).ok();
920 }
921}