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::{
15 find_live_runtime, forget_live_runtime, list_live_runtimes, resolve_live_runtime,
16 DiscoveryQuery, FrontendActions, FrontendConnectionState, FrontendRuntimeDescriptor,
17 FrontendTurnState, HarnessCatalog, HttpFrontendRuntime, LiveRuntimeEndpoint,
18 RuntimeAuthorization, RuntimeClientId, RuntimeControllerLease, RuntimeObserverLease,
19 RuntimePermission, SdkError, SdkOperation, Session,
20};
21use supercode_interchange::catalog::StorageLocator;
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}