1use std::collections::{BTreeSet, HashMap, HashSet};
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::Arc;
23use std::sync::RwLock as StdRwLock;
24#[cfg(not(target_arch = "wasm32"))]
25use std::sync::{Mutex as StdMutex, OnceLock, Weak};
26
27use meerkat_core::lifecycle::{InputId, RunId};
28use meerkat_core::tool_scope::ToolScopeTurnOverlay;
29use meerkat_core::types::SessionId;
30use meerkat_core::{BlobId, BlobPayload, BlobRef, BlobStore, BlobStoreError};
31use meerkat_core::{
32 DeferredToolLoadAuthority, SessionToolVisibilityState, ToolFilter, ToolScopeApplyError,
33 ToolScopeRevision, ToolScopeStageError, ToolVisibilityOwner, ToolVisibilityWitness,
34};
35
36use crate::accept::AcceptOutcome;
37use crate::driver::ephemeral::EphemeralRuntimeDriver;
38use crate::driver::persistent::PersistentRuntimeDriver;
39use crate::identifiers::LogicalRuntimeId;
40use crate::input::Input;
41use crate::input_state::{
42 InputAbandonReason, InputLifecycleState, InputStateSeed, InputTerminalOutcome,
43};
44use crate::meerkat_machine_types::{
45 HydratedSessionLlmState, MeerkatAdmittedInputSnapshot, MeerkatArchiveSnapshot,
46 MeerkatBindingSnapshot, MeerkatCompletionWaiterSnapshot, MeerkatCompletionWaitersSnapshot,
47 MeerkatControlSnapshot, MeerkatCursorSnapshot, MeerkatDrainSnapshot, MeerkatDriverKind,
48 MeerkatFormalStateProjection, MeerkatInputsSnapshot, MeerkatLedgerSnapshot,
49 MeerkatMachineCommand, MeerkatMachineCommandError, MeerkatMachineCommandResult,
50 MeerkatMachineRunFailure, MeerkatMachineSpineSnapshot, MeerkatOpsSnapshot,
51 SessionLlmCapabilityDelta, SessionLlmCapabilitySurface, SessionLlmReconfigureHost,
52 SessionLlmReconfigureReport, SessionLlmReconfigureRequest, SessionToolVisibilityDelta,
53};
54use crate::runtime_state::RuntimeState;
55use crate::service_ext::SessionServiceRuntimeExt;
56use crate::store::RuntimeStore;
57use crate::tokio;
58use crate::tokio::sync::{Mutex, RwLock, mpsc};
59#[cfg(test)]
60use crate::traits::RuntimeDriver;
61use crate::traits::{
62 DestroyReport, RecoveryReport, RecycleReport, ResetReport, RetireReport,
63 RuntimeControlPlaneError, RuntimeDriverError,
64};
65
66#[allow(clippy::expect_used)]
67pub(crate) fn recover_projected_authority(
68 state: dsl::MeerkatMachineState,
69 context: &'static str,
70) -> dsl::MeerkatMachineAuthority {
71 dsl::MeerkatMachineAuthority::recover_from_state(state).expect(context)
72}
73
74struct ToolVisibilityOwnerGeneratedAuthorityBridgeToken;
75
76static TOOL_VISIBILITY_OWNER_GENERATED_AUTHORITY_BRIDGE_TOKEN:
77 ToolVisibilityOwnerGeneratedAuthorityBridgeToken =
78 ToolVisibilityOwnerGeneratedAuthorityBridgeToken;
79
80fn tool_visibility_owner_generated_authority_bridge_token()
81-> &'static (dyn std::any::Any + Send + Sync) {
82 &TOOL_VISIBILITY_OWNER_GENERATED_AUTHORITY_BRIDGE_TOKEN
83}
84
85#[doc(hidden)]
86#[allow(improper_ctypes_definitions, unsafe_code)]
87#[unsafe(export_name = concat!(
88 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_tool_visibility_owner_",
89 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
90))]
91pub extern "Rust" fn tool_visibility_owner_generated_authority_bridge_token_is_valid(
92 token: &(dyn std::any::Any + Send + Sync),
93) -> bool {
94 token.is::<ToolVisibilityOwnerGeneratedAuthorityBridgeToken>()
95}
96
97fn generated_tool_visibility_owner(
98 owner: Arc<dyn ToolVisibilityOwner>,
99) -> Result<meerkat_core::GeneratedToolVisibilityOwner, String> {
100 #[allow(improper_ctypes_definitions, unsafe_code)]
101 unsafe extern "Rust" {
102 #[link_name = concat!(
103 "__meerkat_core_runtime_generated_tool_visibility_owner_build_v1_",
104 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
105 )]
106 fn core_runtime_generated_tool_visibility_owner_build(
107 token: &'static (dyn std::any::Any + Send + Sync),
108 owner: Arc<dyn ToolVisibilityOwner>,
109 ) -> Result<meerkat_core::GeneratedToolVisibilityOwner, String>;
110 }
111 #[allow(unsafe_code)]
112 unsafe {
113 core_runtime_generated_tool_visibility_owner_build(
114 tool_visibility_owner_generated_authority_bridge_token(),
115 owner,
116 )
117 }
118}
119
120pub fn standalone_tool_visibility_owner(
127 session_id: &SessionId,
128 current_identity: &meerkat_core::SessionLlmIdentity,
129 model_profile: Option<&meerkat_core::model_profile::ModelProfile>,
130 capability_base_filter: &ToolFilter,
131) -> Result<meerkat_core::GeneratedToolVisibilityOwner, String> {
132 let mut authority = dsl_authority::recover_authority_from_runtime_observation(
133 session_id,
134 RuntimeState::Idle,
135 None,
136 None,
137 None,
138 BTreeSet::new(),
139 None,
140 None,
141 None,
142 )
143 .map_err(|err| dsl_authority::map_error(err, "standalone visibility authority"))?;
144 let (current_capability_surface, current_capability_surface_status) = match model_profile {
145 Some(profile) => (
146 Some(dsl::SessionLlmCapabilitySurface {
147 supports_temperature: profile.supports_temperature,
148 supports_thinking: profile.supports_thinking,
149 supports_reasoning: profile.supports_reasoning,
150 inline_video: profile.inline_video,
151 vision: profile.vision,
152 image_input: profile.image_input,
153 image_tool_results: profile.image_tool_results,
154 supports_web_search: profile.supports_web_search,
155 image_generation: profile.image_generation,
156 realtime: profile.realtime,
157 call_timeout_secs: profile.call_timeout_secs,
158 }),
159 dsl::SessionLlmCapabilitySurfaceStatus::Resolved,
160 ),
161 None => (None, dsl::SessionLlmCapabilitySurfaceStatus::Unresolved),
162 };
163 dsl::MeerkatMachineMutator::apply(
164 &mut authority,
165 dsl::MeerkatMachineInput::HydrateSessionLlmState {
166 current_identity: dsl::SessionLlmIdentity::from_domain(current_identity),
167 current_capability_surface,
168 current_capability_surface_status,
169 current_capability_base_filter: dsl::ToolFilter::from_domain(capability_base_filter),
170 },
171 )
172 .map_err(|err| dsl_authority::map_error(err, "standalone visibility hydration"))?;
173 let authority = Arc::new(std::sync::Mutex::new(authority));
174 let owner = Arc::new(MachineToolVisibilityOwner::new());
175 owner.bind_dsl_authority(authority);
176 generated_tool_visibility_owner(owner as Arc<dyn ToolVisibilityOwner>)
177}
178
179#[derive(Debug, thiserror::Error)]
181pub enum RuntimeBindingsError {
182 #[error("session {0} not found in runtime adapter after registration")]
184 SessionNotFound(SessionId),
185 #[error("failed to prepare runtime bindings for session {0}: {1}")]
187 PrepareFailed(SessionId, String),
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub struct InputPublicStateProjection {
193 pub lifecycle_state: dsl::InputPublicLifecycleState,
194 pub terminal_outcome: Option<dsl::InputPublicTerminalOutcome>,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub struct RuntimeLifecycleFacts {
201 pub terminality: dsl::RuntimeLifecycleTerminality,
202 pub input_admission: dsl::RuntimeInputAdmission,
203 pub queue_admission: dsl::RuntimeQueueAdmission,
204 pub prepare_admission: dsl::RuntimePrepareAdmission,
205 pub ingress_admission: dsl::RuntimeIngressAdmission,
206}
207
208impl RuntimeLifecycleFacts {
209 #[must_use]
210 pub fn can_accept_input(self) -> bool {
211 self.input_admission == dsl::RuntimeInputAdmission::AcceptsInput
212 }
213
214 #[must_use]
215 pub fn can_process_queue(self) -> bool {
216 self.queue_admission == dsl::RuntimeQueueAdmission::ProcessesQueue
217 }
218
219 #[must_use]
220 pub fn can_prepare_run(self) -> bool {
221 self.prepare_admission == dsl::RuntimePrepareAdmission::Ready
222 }
223
224 #[must_use]
225 pub fn is_terminal(self) -> bool {
226 self.terminality == dsl::RuntimeLifecycleTerminality::Terminal
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub struct RuntimeLoopQueueAdmissionPlan {
234 pub queue_admission: dsl::RuntimeQueueAdmission,
235 pub run_binding: dsl::RuntimeLoopRunBinding,
236}
237
238impl RuntimeLoopQueueAdmissionPlan {
239 #[must_use]
240 pub fn can_process_queue(self) -> bool {
241 self.queue_admission == dsl::RuntimeQueueAdmission::ProcessesQueue
242 }
243
244 #[must_use]
245 pub fn uses_prebound_run(self) -> bool {
246 self.run_binding == dsl::RuntimeLoopRunBinding::UsePrebound
247 }
248}
249
250pub fn classify_runtime_lifecycle_state(
254 state: RuntimeState,
255) -> Result<RuntimeLifecycleFacts, String> {
256 let observed_state = dsl_authority::observed_runtime_lifecycle_state(state);
257 let mut authority = projection_authority();
258 let transition = dsl::MeerkatMachineMutator::apply(
259 &mut authority,
260 dsl::MeerkatMachineInput::ClassifyRuntimeLifecycleState {
261 state: observed_state,
262 },
263 )
264 .map_err(|err| {
265 format!("MeerkatMachine rejected runtime lifecycle classification for {state}: {err}")
266 })?;
267
268 transition
269 .into_effects()
270 .into_iter()
271 .find_map(|effect| match effect {
272 dsl::MeerkatMachineEffect::RuntimeLifecycleStateClassified {
273 state,
274 terminality,
275 input_admission,
276 queue_admission,
277 prepare_admission,
278 ingress_admission,
279 } if state == observed_state => Some(RuntimeLifecycleFacts {
280 terminality,
281 input_admission,
282 queue_admission,
283 prepare_admission,
284 ingress_admission,
285 }),
286 _ => None,
287 })
288 .ok_or_else(|| {
289 format!("MeerkatMachine emitted no runtime lifecycle classification for {state}")
290 })
291}
292
293pub fn classify_runtime_lifecycle_durable_state(
297 state: RuntimeState,
298) -> Result<RuntimeState, String> {
299 let observed_state = dsl_authority::observed_runtime_lifecycle_state(state);
300 let mut authority = projection_authority();
301 let transition = dsl::MeerkatMachineMutator::apply(
302 &mut authority,
303 dsl::MeerkatMachineInput::ClassifyRuntimeLifecycleDurability {
304 state: observed_state,
305 },
306 )
307 .map_err(|err| {
308 format!(
309 "MeerkatMachine rejected runtime lifecycle durability classification for {state}: {err}"
310 )
311 })?;
312
313 transition
314 .into_effects()
315 .into_iter()
316 .find_map(|effect| match effect {
317 dsl::MeerkatMachineEffect::RuntimeLifecycleDurabilityClassified {
318 state,
319 durable_state,
320 } if state == observed_state => Some(
321 dsl_authority::runtime_state_from_observed_lifecycle_state(durable_state),
322 ),
323 _ => None,
324 })
325 .ok_or_else(|| {
326 format!(
327 "MeerkatMachine emitted no runtime lifecycle durability classification for {state}"
328 )
329 })
330}
331
332pub fn classify_runtime_loop_queue_admission(
337 state: RuntimeState,
338 current_run_bound: bool,
339) -> Result<RuntimeLoopQueueAdmissionPlan, String> {
340 let observed_state = dsl_authority::observed_runtime_lifecycle_state(state);
341 let mut authority = projection_authority();
342 let transition = dsl::MeerkatMachineMutator::apply(
343 &mut authority,
344 dsl::MeerkatMachineInput::ClassifyRuntimeLoopQueueAdmission {
345 state: observed_state,
346 current_run_bound,
347 },
348 )
349 .map_err(|err| {
350 format!(
351 "MeerkatMachine rejected runtime-loop queue admission for {state} with current_run_bound={current_run_bound}: {err}"
352 )
353 })?;
354
355 transition
356 .into_effects()
357 .into_iter()
358 .find_map(|effect| match effect {
359 dsl::MeerkatMachineEffect::RuntimeLoopQueueAdmissionClassified {
360 state,
361 current_run_bound: observed_current_run_bound,
362 queue_admission,
363 run_binding,
364 } if state == observed_state && observed_current_run_bound == current_run_bound => {
365 Some(RuntimeLoopQueueAdmissionPlan {
366 queue_admission,
367 run_binding,
368 })
369 }
370 _ => None,
371 })
372 .ok_or_else(|| {
373 format!(
374 "MeerkatMachine emitted no runtime-loop queue admission for {state} with current_run_bound={current_run_bound}"
375 )
376 })
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub struct VisibleRuntimePhasePlan {
388 pub publish_control: bool,
389 pub selected_raw_phase: RuntimeState,
390 pub visible_phase: RuntimeState,
391}
392
393pub fn resolve_visible_runtime_phase(
401 dsl_phase: RuntimeState,
402 dsl_pre_run_phase: Option<RuntimeState>,
403 control_phase: RuntimeState,
404 control_pre_run_phase: Option<RuntimeState>,
405 has_runtime_persistence: bool,
406) -> Result<VisibleRuntimePhasePlan, String> {
407 let observed_dsl = dsl_authority::observed_runtime_lifecycle_state(dsl_phase);
408 let observed_control = dsl_authority::observed_runtime_lifecycle_state(control_phase);
409 let observed_dsl_pre_run =
410 dsl_pre_run_phase.map(dsl_authority::observed_runtime_lifecycle_state);
411 let observed_control_pre_run =
412 control_pre_run_phase.map(dsl_authority::observed_runtime_lifecycle_state);
413 let mut authority = projection_authority();
414 let transition = dsl::MeerkatMachineMutator::apply(
415 &mut authority,
416 dsl::MeerkatMachineInput::ResolveVisibleRuntimePhase {
417 dsl_phase: observed_dsl,
418 dsl_pre_run_phase: observed_dsl_pre_run,
419 control_phase: observed_control,
420 control_pre_run_phase: observed_control_pre_run,
421 has_runtime_persistence,
422 },
423 )
424 .map_err(|err| {
425 format!(
426 "MeerkatMachine rejected visible runtime phase resolution \
427 (dsl={dsl_phase}, control={control_phase}, persistence={has_runtime_persistence}): {err}"
428 )
429 })?;
430
431 transition
432 .into_effects()
433 .into_iter()
434 .find_map(|effect| match effect {
435 dsl::MeerkatMachineEffect::VisibleRuntimePhaseResolved {
436 publish_control,
437 selected_raw_phase,
438 visible_phase,
439 } => Some(VisibleRuntimePhasePlan {
440 publish_control,
441 selected_raw_phase: dsl_authority::runtime_state_from_observed_lifecycle_state(
442 selected_raw_phase,
443 ),
444 visible_phase: dsl_authority::runtime_state_from_observed_lifecycle_state(
445 visible_phase,
446 ),
447 }),
448 _ => None,
449 })
450 .ok_or_else(|| {
451 format!(
452 "MeerkatMachine emitted no visible runtime phase resolution \
453 (dsl={dsl_phase}, control={control_phase}, persistence={has_runtime_persistence})"
454 )
455 })
456}
457
458pub fn resolve_input_public_lifecycle_projection(
461 input_id: &InputId,
462 phase: InputLifecycleState,
463) -> Result<dsl::InputPublicLifecycleState, String> {
464 let input_key = input_id.to_string();
465 let mut authority = projection_authority();
466 let transition = dsl::MeerkatMachineMutator::apply(
467 &mut authority,
468 dsl::MeerkatMachineInput::ResolveInputPublicLifecycle {
469 input_id: input_key.clone(),
470 phase: observed_input_phase(phase),
471 },
472 )
473 .map_err(|err| {
474 format!("MeerkatMachine rejected public lifecycle projection for '{input_id}': {err}")
475 })?;
476
477 transition
478 .into_effects()
479 .into_iter()
480 .find_map(|effect| match effect {
481 dsl::MeerkatMachineEffect::InputPublicLifecycleResolved { input_id, phase }
482 if input_id == input_key =>
483 {
484 Some(phase)
485 }
486 _ => None,
487 })
488 .ok_or_else(|| {
489 format!("MeerkatMachine emitted no public lifecycle projection for '{input_id}'")
490 })
491}
492
493pub fn resolve_input_public_state_projection(
496 input_id: &InputId,
497 seed: &InputStateSeed,
498) -> Result<InputPublicStateProjection, String> {
499 let lifecycle_state = resolve_input_public_lifecycle_projection(input_id, seed.phase)?;
500 let terminal_outcome = resolve_input_public_terminal_projection(input_id, seed)?;
501 Ok(InputPublicStateProjection {
502 lifecycle_state,
503 terminal_outcome,
504 })
505}
506
507pub(crate) fn input_seed_behavioral_terminality_via_authority(
508 input_id: &InputId,
509 seed: &InputStateSeed,
510) -> Result<bool, String> {
511 classify_input_behavioral_terminality(input_id, seed.phase, seed.terminal_outcome.as_ref())
512}
513
514pub(crate) fn input_phase_behavioral_terminality_via_authority(
515 input_id: &InputId,
516 phase: InputLifecycleState,
517 terminal_outcome: Option<InputTerminalOutcome>,
518) -> Result<bool, String> {
519 classify_input_behavioral_terminality(input_id, phase, terminal_outcome.as_ref())
520}
521
522pub(crate) fn authorize_stored_input_state_seed(
525 input_id: &InputId,
526 seed: &InputStateSeed,
527) -> Result<(), String> {
528 let input_key = input_id.to_string();
529 let (terminal_kind, superseded_by, aggregate_id, abandon_reason, abandon_attempt_count) =
530 input_seed_terminal_parts(seed)?;
531 let mut authority = projection_authority();
532 let transition = dsl::MeerkatMachineMutator::apply(
533 &mut authority,
534 dsl::MeerkatMachineInput::AuthorizeStoredInputStateSeed {
535 input_id: input_key.clone(),
536 phase: observed_input_phase(seed.phase),
537 terminal_kind,
538 superseded_by,
539 aggregate_id,
540 abandon_reason,
541 abandon_attempt_count,
542 attempt_count: u64::from(seed.attempt_count),
543 run_id: seed.last_run_id.as_ref().map(dsl::RunId::from_domain),
544 boundary_sequence: seed.last_boundary_sequence,
545 admission_sequence: seed.admission_sequence,
546 recovery_lane: seed.recovery_lane.map(dsl::InputLane::from),
547 },
548 )
549 .map_err(|err| {
550 format!("MeerkatMachine rejected stored input-state seed for '{input_id}': {err}")
551 })?;
552
553 transition
554 .into_effects()
555 .into_iter()
556 .find_map(|effect| match effect {
557 dsl::MeerkatMachineEffect::StoredInputStateSeedAuthorized { input_id }
558 if input_id == input_key =>
559 {
560 Some(())
561 }
562 _ => None,
563 })
564 .ok_or_else(|| {
565 format!("MeerkatMachine emitted no stored input-state seed authority for '{input_id}'")
566 })
567}
568
569fn classify_input_behavioral_terminality(
570 input_id: &InputId,
571 phase: InputLifecycleState,
572 terminal_outcome: Option<&InputTerminalOutcome>,
573) -> Result<bool, String> {
574 let input_key = input_id.to_string();
575 let (terminal_kind, abandon_reason) = input_terminality_parts(terminal_outcome);
576 let mut authority = projection_authority();
577 let transition = dsl::MeerkatMachineMutator::apply(
578 &mut authority,
579 dsl::MeerkatMachineInput::ClassifyInputTerminality {
580 input_id: input_key.clone(),
581 phase: observed_input_phase(phase),
582 terminal_kind,
583 abandon_reason,
584 },
585 )
586 .map_err(|err| {
587 format!("MeerkatMachine rejected behavioral input terminality for '{input_id}': {err}")
588 })?;
589
590 let mut terminality = None;
591 for effect in transition.into_effects() {
592 match effect {
593 dsl::MeerkatMachineEffect::InputBehavioralTerminalityResolved {
594 input_id,
595 terminal,
596 } if input_id == input_key => terminality = Some(terminal),
597 other => {
598 return Err(format!(
599 "MeerkatMachine emitted unexpected behavioral input terminality effect for '{input_id}': {other:?}"
600 ));
601 }
602 }
603 }
604 terminality.ok_or_else(|| {
605 format!("MeerkatMachine emitted no behavioral input terminality for '{input_id}'")
606 })
607}
608
609fn resolve_input_public_terminal_projection(
610 input_id: &InputId,
611 seed: &InputStateSeed,
612) -> Result<Option<dsl::InputPublicTerminalOutcome>, String> {
613 let input_key = input_id.to_string();
614 let (terminal_kind, abandon_reason) = input_terminality_parts(seed.terminal_outcome.as_ref());
615 let mut authority = projection_authority();
616 let transition = dsl::MeerkatMachineMutator::apply(
617 &mut authority,
618 dsl::MeerkatMachineInput::ResolveInputPublicTerminalOutcome {
619 input_id: input_key.clone(),
620 phase: observed_input_phase(seed.phase),
621 terminal_kind,
622 abandon_reason,
623 },
624 )
625 .map_err(|err| {
626 format!("MeerkatMachine rejected public terminal projection for '{input_id}': {err}")
627 })?;
628
629 transition
630 .into_effects()
631 .into_iter()
632 .find_map(|effect| match effect {
633 dsl::MeerkatMachineEffect::InputPublicTerminalOutcomeResolved {
634 input_id,
635 terminal_outcome,
636 } if input_id == input_key => Some(terminal_outcome),
637 _ => None,
638 })
639 .ok_or_else(|| {
640 format!("MeerkatMachine emitted no public terminal projection for '{input_id}'")
641 })
642}
643
644fn projection_authority() -> dsl::MeerkatMachineAuthority {
645 dsl_authority::new_initialized_authority("projection authority must initialize")
646}
647
648#[cfg(feature = "live")]
649fn live_unbound_rejection_authority() -> crate::driver::ephemeral::SharedIngressDslAuthority {
650 Arc::new(std::sync::Mutex::new(
651 dsl_authority::new_initialized_authority(
652 "live unbound rejection authority must initialize",
653 ),
654 ))
655}
656
657fn observed_input_phase(phase: InputLifecycleState) -> dsl::RecoveredInputObservedPhase {
658 match phase {
659 InputLifecycleState::Accepted => dsl::RecoveredInputObservedPhase::Accepted,
660 InputLifecycleState::Queued => dsl::RecoveredInputObservedPhase::Queued,
661 InputLifecycleState::Staged => dsl::RecoveredInputObservedPhase::Staged,
662 InputLifecycleState::Applied => dsl::RecoveredInputObservedPhase::Applied,
663 InputLifecycleState::AppliedPendingConsumption => {
664 dsl::RecoveredInputObservedPhase::AppliedPendingConsumption
665 }
666 InputLifecycleState::Consumed => dsl::RecoveredInputObservedPhase::Consumed,
667 InputLifecycleState::Superseded => dsl::RecoveredInputObservedPhase::Superseded,
668 InputLifecycleState::Coalesced => dsl::RecoveredInputObservedPhase::Coalesced,
669 InputLifecycleState::Abandoned => dsl::RecoveredInputObservedPhase::Abandoned,
670 }
671}
672
673type InputSeedTerminalParts = (
674 Option<dsl::InputTerminalKind>,
675 Option<String>,
676 Option<String>,
677 Option<dsl::InputAbandonReason>,
678 u64,
679);
680
681fn input_seed_terminal_parts(seed: &InputStateSeed) -> Result<InputSeedTerminalParts, String> {
682 match seed.terminal_outcome.as_ref() {
683 None => Ok((None, None, None, None, 0)),
684 Some(InputTerminalOutcome::Consumed) => {
685 Ok((Some(dsl::InputTerminalKind::Consumed), None, None, None, 0))
686 }
687 Some(InputTerminalOutcome::Superseded { superseded_by }) => Ok((
688 Some(dsl::InputTerminalKind::Superseded),
689 Some(superseded_by.to_string()),
690 None,
691 None,
692 0,
693 )),
694 Some(InputTerminalOutcome::Coalesced { aggregate_id }) => Ok((
695 Some(dsl::InputTerminalKind::Coalesced),
696 None,
697 Some(aggregate_id.to_string()),
698 None,
699 0,
700 )),
701 Some(InputTerminalOutcome::Abandoned { reason }) => {
702 let abandon_attempt_count = match reason {
703 InputAbandonReason::MaxAttemptsExhausted { attempts } => u64::from(*attempts),
704 _ => u64::from(seed.attempt_count),
705 };
706 Ok((
707 Some(dsl::InputTerminalKind::Abandoned),
708 None,
709 None,
710 input_terminality_parts(seed.terminal_outcome.as_ref()).1,
711 abandon_attempt_count,
712 ))
713 }
714 }
715}
716
717fn input_terminality_parts(
718 outcome: Option<&InputTerminalOutcome>,
719) -> (
720 Option<dsl::InputTerminalKind>,
721 Option<dsl::InputAbandonReason>,
722) {
723 match outcome {
724 None => (None, None),
725 Some(InputTerminalOutcome::Consumed) => (Some(dsl::InputTerminalKind::Consumed), None),
726 Some(InputTerminalOutcome::Superseded { .. }) => {
727 (Some(dsl::InputTerminalKind::Superseded), None)
728 }
729 Some(InputTerminalOutcome::Coalesced { .. }) => {
730 (Some(dsl::InputTerminalKind::Coalesced), None)
731 }
732 Some(InputTerminalOutcome::Abandoned { reason }) => (
733 Some(dsl::InputTerminalKind::Abandoned),
734 Some(match reason {
735 InputAbandonReason::Retired => dsl::InputAbandonReason::Retired,
736 InputAbandonReason::Reset => dsl::InputAbandonReason::Reset,
737 InputAbandonReason::Stopped => dsl::InputAbandonReason::Stopped,
738 InputAbandonReason::Destroyed => dsl::InputAbandonReason::Destroyed,
739 InputAbandonReason::Cancelled => dsl::InputAbandonReason::Cancelled,
740 InputAbandonReason::MaxAttemptsExhausted { .. } => {
741 dsl::InputAbandonReason::MaxAttemptsExhausted
742 }
743 }),
744 ),
745 }
746}
747
748#[derive(Debug, Default)]
749struct UnavailableBlobStore;
750
751impl UnavailableBlobStore {
752 fn error() -> BlobStoreError {
753 BlobStoreError::Unsupported(
754 "persistent runtime constructed without blob store; blob-backed inputs require a BlobStore"
755 .to_string(),
756 )
757 }
758}
759
760#[cfg(not(target_arch = "wasm32"))]
761struct PersistentAuthAuthorityBundle {
762 store: StdMutex<Weak<dyn RuntimeStore>>,
763 auth_lease: Arc<crate::handles::RuntimeAuthLeaseHandle>,
764 oauth_flows: Arc<crate::handles::RuntimeOAuthFlowHandle>,
765}
766
767#[cfg(not(target_arch = "wasm32"))]
768#[derive(Debug, Clone, PartialEq, Eq, Hash)]
769enum PersistentAuthAuthorityKey {
770 Durable(String),
771 Process(usize),
772}
773
774#[cfg(not(target_arch = "wasm32"))]
775static PERSISTENT_AUTH_AUTHORITIES: OnceLock<
776 StdMutex<HashMap<PersistentAuthAuthorityKey, Arc<PersistentAuthAuthorityBundle>>>,
777> = OnceLock::new();
778
779#[cfg(not(target_arch = "wasm32"))]
780fn runtime_store_identity(store: &Arc<dyn RuntimeStore>) -> PersistentAuthAuthorityKey {
781 store
782 .auth_authority_key()
783 .map(PersistentAuthAuthorityKey::Durable)
784 .unwrap_or_else(|| {
785 PersistentAuthAuthorityKey::Process(Arc::as_ptr(store).cast::<()>() as usize)
786 })
787}
788
789fn runtime_stores_share_authority(a: &Arc<dyn RuntimeStore>, b: &Arc<dyn RuntimeStore>) -> bool {
790 match (a.auth_authority_key(), b.auth_authority_key()) {
791 (Some(a), Some(b)) => a == b,
792 _ => Arc::ptr_eq(a, b),
793 }
794}
795
796fn generated_runtime_auth_lease_handle(
797 handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
798) -> meerkat_core::handles::GeneratedAuthLeaseHandle {
799 #[allow(clippy::expect_used)]
800 crate::protocol_auth_lease_lifecycle_publication::generated_auth_lease_handle(handle)
801 .expect("runtime AuthLeaseHandle must be certified by generated AuthMachine authority")
802}
803
804#[cfg(not(target_arch = "wasm32"))]
805fn persistent_auth_authorities(
806 store: &Arc<dyn RuntimeStore>,
807) -> Arc<PersistentAuthAuthorityBundle> {
808 let key = runtime_store_identity(store);
809 let authorities = PERSISTENT_AUTH_AUTHORITIES.get_or_init(|| StdMutex::new(HashMap::new()));
810 let mut authorities = authorities
811 .lock()
812 .unwrap_or_else(std::sync::PoisonError::into_inner);
813 if let Some(existing) = authorities.get(&key) {
814 let stored_store_alive = existing
815 .store
816 .lock()
817 .unwrap_or_else(std::sync::PoisonError::into_inner)
818 .upgrade()
819 .is_some();
820 if matches!(key, PersistentAuthAuthorityKey::Durable(_)) || stored_store_alive {
821 existing.oauth_flows.bind_persistent_store(store);
822 *existing
823 .store
824 .lock()
825 .unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::downgrade(store);
826 return Arc::clone(existing);
827 }
828 }
829 let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
830 let oauth_flows = Arc::new(
831 crate::handles::RuntimeOAuthFlowHandle::new_with_persistent_store_and_auth_lease(
832 std::time::Duration::from_secs(10 * 60),
833 Arc::clone(&auth_lease),
834 store,
835 ),
836 );
837 let bundle = Arc::new(PersistentAuthAuthorityBundle {
838 store: StdMutex::new(Arc::downgrade(store)),
839 auth_lease,
840 oauth_flows,
841 });
842 authorities.insert(key, Arc::clone(&bundle));
843 bundle
844}
845
846#[cfg(all(test, not(target_arch = "wasm32")))]
847pub(crate) fn clear_persistent_auth_authorities_for_test() {
848 if let Some(authorities) = PERSISTENT_AUTH_AUTHORITIES.get() {
849 authorities
850 .lock()
851 .unwrap_or_else(std::sync::PoisonError::into_inner)
852 .clear();
853 }
854}
855
856#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
857#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
858impl BlobStore for UnavailableBlobStore {
859 async fn put_image(&self, _media_type: &str, _data: &str) -> Result<BlobRef, BlobStoreError> {
860 Err(Self::error())
861 }
862
863 async fn get(&self, _blob_id: &BlobId) -> Result<BlobPayload, BlobStoreError> {
864 Err(Self::error())
865 }
866
867 async fn delete(&self, _blob_id: &BlobId) -> Result<(), BlobStoreError> {
868 Err(Self::error())
869 }
870
871 async fn exists(&self, _blob_id: &BlobId) -> Result<bool, BlobStoreError> {
872 Err(Self::error())
873 }
874
875 fn is_persistent(&self) -> bool {
876 false
877 }
878}
879
880#[cfg(not(target_arch = "wasm32"))]
881type MeerkatMachineCommandFuture<'a> = Pin<
882 Box<
883 dyn Future<Output = Result<MeerkatMachineCommandResult, MeerkatMachineCommandError>>
884 + Send
885 + 'a,
886 >,
887>;
888
889#[cfg(target_arch = "wasm32")]
890type MeerkatMachineCommandFuture<'a> = Pin<
891 Box<dyn Future<Output = Result<MeerkatMachineCommandResult, MeerkatMachineCommandError>> + 'a>,
892>;
893
894pub(crate) use driver::{
895 DriverEntry, SharedCompletionRegistry, SharedDriver, cancel_runtime_loop_run,
896 commit_runtime_loop_run, fail_machine_run, fail_runtime_loop_run,
897 machine_authorize_runtime_loop_batch, machine_batch_primitive_projections,
898 machine_batch_runtime_semantics, machine_commit_prepared_destroy,
899 machine_commit_service_turn_terminal_receipt, machine_prepare_bindings_projection,
900 machine_prepare_destroy, machine_recover_ephemeral_driver, machine_recover_persistent_driver,
901 machine_recycle_preserving_work, machine_reset, machine_retire, machine_stop_runtime,
902 prepare_runtime_loop_batch_start,
903};
904
905pub(crate) mod driver;
906
907mod comms_drain;
908pub mod composition;
909mod dispatch_control;
910mod dispatch_drain;
911mod dispatch_ingress;
912mod dispatch_session;
913#[allow(unused_variables, dead_code, clippy::cmp_owned)]
914#[allow(clippy::assign_op_pattern)]
915pub mod dsl;
916pub(crate) mod dsl_authority;
917mod dsl_effects;
918mod llm_reconfigure;
919mod runtime_control;
920mod session_management;
921mod traits;
922mod visibility;
923
924pub use composition::{MeerkatCompositionSignalDispatcher, MeerkatConsumerSurface};
925
926pub use comms_drain::{
927 CommsDrainMode, CommsDrainPhase, DrainExitReason, PeerEndpointStageError, PeerIngressOwner,
928 SupervisorBinding, SupervisorBindingStageError,
929};
930pub(crate) use comms_drain::{
931 CommsDrainSlot, SupervisorAuthorizeAdmission, SupervisorBindAdmission,
932 SupervisorBridgeCommandAdmission, abort_slot,
933};
934pub(crate) use dsl_effects::{DslTransitionEffects, apply_dsl_transition_on_authority};
935pub(crate) use visibility::MachineToolVisibilityOwner;
936
937struct StagedSessionDslInput {
938 previous_snapshot: dsl::MeerkatMachineAuthoritySnapshot,
939 committed_snapshot: dsl::MeerkatMachineAuthoritySnapshot,
940 effects: DslTransitionEffects,
941}
942
943#[derive(Clone, Copy)]
944enum CommittedEffectDispatchFailure {
945 PreserveCommittedDslState,
946}
947
948struct RuntimeSessionEntry {
950 runtime_id: LogicalRuntimeId,
952 mutation_gate: Arc<Mutex<()>>,
964 driver: SharedDriver,
966 control_projection: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
972 ops_lifecycle: Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
974 epoch_id: meerkat_core::RuntimeEpochId,
976 handle_teardown_gate: Arc<crate::handles::HandleTeardownGate>,
981 cursor_state: Arc<meerkat_core::EpochCursorState>,
983 completions: SharedCompletionRegistry,
985 tool_visibility_owner: Arc<MachineToolVisibilityOwner>,
987 attachment_slot: RuntimeLoopAttachmentSlot,
992 provisional_interrupt_handle:
995 Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
996 dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1006 drain_slot: CommsDrainSlot,
1016}
1017
1018struct RuntimeLoopAttachment {
1023 wake_tx: mpsc::Sender<()>,
1024 effect_tx: mpsc::Sender<crate::effect::RuntimeEffect>,
1025 boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
1026 interrupt_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
1027 loop_handle: tokio::task::JoinHandle<()>,
1028}
1029
1030enum RuntimeLoopAttachmentSlot {
1032 Empty,
1033 Attached(RuntimeLoopAttachment),
1034}
1035
1036impl RuntimeSessionEntry {
1037 fn control_snapshot(&self) -> crate::driver::ephemeral::RuntimeControlProjection {
1038 self.control_projection
1039 .read()
1040 .map(|guard| guard.clone())
1041 .unwrap_or_else(|poisoned| {
1042 tracing::error!("runtime control projection lock poisoned");
1043 poisoned.into_inner().clone()
1044 })
1045 }
1046
1047 fn attachment_is_live(&self) -> bool {
1048 match &self.attachment_slot {
1049 RuntimeLoopAttachmentSlot::Attached(attachment) => {
1050 !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed()
1051 }
1052 RuntimeLoopAttachmentSlot::Empty => false,
1053 }
1054 }
1055
1056 fn generated_executor_registration_active(&self) -> bool {
1057 let authority = self
1058 .dsl_authority
1059 .lock()
1060 .unwrap_or_else(std::sync::PoisonError::into_inner);
1061 matches!(
1062 authority.state().registration_phase,
1063 dsl::RegistrationPhase::Active
1064 )
1065 }
1066
1067 fn close_handle_teardown_gate(&self) {
1068 let _guard = self
1069 .dsl_authority
1070 .lock()
1071 .unwrap_or_else(std::sync::PoisonError::into_inner);
1072 self.handle_teardown_gate.close();
1073 }
1074
1075 fn generated_executor_registration_active_or_draining(&self) -> bool {
1083 let authority = self
1084 .dsl_authority
1085 .lock()
1086 .unwrap_or_else(std::sync::PoisonError::into_inner);
1087 matches!(
1088 authority.state().registration_phase,
1089 dsl::RegistrationPhase::Active | dsl::RegistrationPhase::Draining
1090 )
1091 }
1092
1093 fn generated_stop_deferred(&self) -> bool {
1094 self.dsl_authority
1095 .lock()
1096 .unwrap_or_else(std::sync::PoisonError::into_inner)
1097 .state()
1098 .runtime_stop_deferred
1099 }
1100
1101 fn stage_generated_executor_registration_claim(
1102 &self,
1103 session_id: &SessionId,
1104 ) -> Result<StagedSessionDslInput, String> {
1105 let staged = MeerkatMachine::stage_dsl_transition_on_authority(
1106 &self.dsl_authority,
1107 dsl::MeerkatMachineInput::EnsureSessionWithExecutor {
1108 session_id: dsl::SessionId::from_domain(session_id),
1109 },
1110 "EnsureSessionWithExecutor",
1111 )?;
1112 if self.generated_executor_registration_active() {
1113 Ok(staged)
1114 } else {
1115 let mut authority = self
1116 .dsl_authority
1117 .lock()
1118 .unwrap_or_else(std::sync::PoisonError::into_inner);
1119 authority.restore_snapshot(staged.previous_snapshot);
1120 Err("generated MeerkatMachine did not grant active executor registration".into())
1121 }
1122 }
1123
1124 fn stage_generated_executor_exit_observation(&self) -> Result<StagedSessionDslInput, String> {
1125 MeerkatMachine::stage_runtime_internal_dsl_transition_on_authority(
1126 &self.dsl_authority,
1127 crate::meerkat_machine_types::MeerkatMachineFieldlessRuntimeInternalInput::RuntimeExecutorExited,
1128 )
1129 }
1130
1131 fn has_live_attachment(&self) -> bool {
1134 self.attachment_is_live()
1135 }
1136
1137 fn attach_runtime_loop(
1138 &mut self,
1139 wake_tx: mpsc::Sender<()>,
1140 effect_tx: mpsc::Sender<crate::effect::RuntimeEffect>,
1141 boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
1142 interrupt_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
1143 loop_handle: tokio::task::JoinHandle<()>,
1144 ) {
1145 self.provisional_interrupt_handle = None;
1146 self.attachment_slot = RuntimeLoopAttachmentSlot::Attached(RuntimeLoopAttachment {
1147 wake_tx,
1148 effect_tx,
1149 boundary_handle,
1150 interrupt_handle,
1151 loop_handle,
1152 });
1153 }
1154
1155 fn take_loop_join_handle(&mut self) -> Option<tokio::task::JoinHandle<()>> {
1163 match std::mem::replace(&mut self.attachment_slot, RuntimeLoopAttachmentSlot::Empty) {
1164 RuntimeLoopAttachmentSlot::Attached(attachment) => Some(attachment.loop_handle),
1165 RuntimeLoopAttachmentSlot::Empty => None,
1166 }
1167 }
1168
1169 fn clear_dead_attachment(&mut self) -> bool {
1170 if matches!(self.attachment_slot, RuntimeLoopAttachmentSlot::Attached(_))
1171 && !self.attachment_is_live()
1172 {
1173 self.attachment_slot = RuntimeLoopAttachmentSlot::Empty;
1174 return true;
1175 }
1176 false
1177 }
1178
1179 fn wake_sender(&self) -> Option<mpsc::Sender<()>> {
1180 match &self.attachment_slot {
1181 RuntimeLoopAttachmentSlot::Attached(attachment)
1182 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1183 {
1184 Some(attachment.wake_tx.clone())
1185 }
1186 _ => None,
1187 }
1188 }
1189
1190 fn effect_sender(&self) -> Option<mpsc::Sender<crate::effect::RuntimeEffect>> {
1191 match &self.attachment_slot {
1192 RuntimeLoopAttachmentSlot::Attached(attachment)
1193 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1194 {
1195 Some(attachment.effect_tx.clone())
1196 }
1197 _ => None,
1198 }
1199 }
1200
1201 fn boundary_handle(
1202 &self,
1203 ) -> Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>> {
1204 match &self.attachment_slot {
1205 RuntimeLoopAttachmentSlot::Attached(attachment)
1206 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1207 {
1208 attachment.boundary_handle.clone()
1209 }
1210 _ => None,
1211 }
1212 }
1213
1214 fn interrupt_handle(
1215 &self,
1216 ) -> Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>> {
1217 match &self.attachment_slot {
1218 RuntimeLoopAttachmentSlot::Attached(attachment)
1219 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1220 {
1221 attachment.interrupt_handle.clone()
1222 }
1223 _ => self.provisional_interrupt_handle.clone(),
1224 }
1225 }
1226
1227 fn install_provisional_interrupt_handle(
1228 &mut self,
1229 handle: Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>,
1230 ) {
1231 if !self.attachment_is_live() {
1232 self.provisional_interrupt_handle = Some(handle);
1233 }
1234 }
1235}
1236
1237impl MeerkatMachine {
1238 async fn session_mutation_gate(&self, session_id: &SessionId) -> Option<Arc<Mutex<()>>> {
1244 let sessions = self.sessions.read().await;
1245 sessions
1246 .get(session_id)
1247 .map(|entry| Arc::clone(&entry.mutation_gate))
1248 }
1249
1250 async fn lock_current_session_mutation_gate(
1251 &self,
1252 session_id: &SessionId,
1253 ) -> Option<crate::tokio::sync::OwnedMutexGuard<()>> {
1254 loop {
1255 let gate = self.session_mutation_gate(session_id).await?;
1256 let gate_guard = Arc::clone(&gate).lock_owned().await;
1257 let sessions = self.sessions.read().await;
1258 let entry = sessions.get(session_id)?;
1259 if Arc::ptr_eq(&entry.mutation_gate, &gate) {
1260 return Some(gate_guard);
1261 }
1262 }
1263 }
1264
1265 pub(crate) async fn lock_current_session_driver_gate(
1266 &self,
1267 session_id: &SessionId,
1268 driver: &SharedDriver,
1269 ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
1270 let gate_guard = self
1271 .lock_current_session_mutation_gate(session_id)
1272 .await
1273 .ok_or(RuntimeDriverError::NotReady {
1274 state: RuntimeState::Destroyed,
1275 })?;
1276 {
1277 let sessions = self.sessions.read().await;
1278 let entry = sessions
1279 .get(session_id)
1280 .ok_or(RuntimeDriverError::NotReady {
1281 state: RuntimeState::Destroyed,
1282 })?;
1283 if !Arc::ptr_eq(&entry.driver, driver) {
1284 return Err(RuntimeDriverError::NotReady {
1285 state: RuntimeState::Destroyed,
1286 });
1287 }
1288 }
1289 Ok(gate_guard)
1290 }
1291
1292 pub(crate) async fn lock_current_runtime_loop_driver_authority(
1293 &self,
1294 session_id: &SessionId,
1295 driver: &SharedDriver,
1296 ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
1297 let gate_guard = self
1298 .lock_current_session_driver_gate(session_id, driver)
1299 .await?;
1300 {
1301 let sessions = self.sessions.read().await;
1302 let entry = sessions
1303 .get(session_id)
1304 .ok_or(RuntimeDriverError::NotReady {
1305 state: RuntimeState::Destroyed,
1306 })?;
1307 if !entry.generated_executor_registration_active_or_draining() {
1308 return Err(RuntimeDriverError::ValidationFailed {
1309 reason:
1310 "generated MeerkatMachine has no active runtime-loop executor registration"
1311 .to_string(),
1312 });
1313 }
1314 }
1315 Ok(gate_guard)
1316 }
1317
1318 async fn current_session_driver_with_authority(
1319 &self,
1320 session_id: &SessionId,
1321 ) -> Result<(SharedDriver, crate::tokio::sync::OwnedMutexGuard<()>), RuntimeDriverError> {
1322 let gate_guard = self
1323 .lock_current_session_mutation_gate(session_id)
1324 .await
1325 .ok_or(RuntimeDriverError::NotReady {
1326 state: RuntimeState::Destroyed,
1327 })?;
1328 let driver = {
1329 let sessions = self.sessions.read().await;
1330 sessions
1331 .get(session_id)
1332 .ok_or(RuntimeDriverError::NotReady {
1333 state: RuntimeState::Destroyed,
1334 })?
1335 .driver
1336 .clone()
1337 };
1338 Ok((driver, gate_guard))
1339 }
1340
1341 async fn session_dsl_authority(
1342 &self,
1343 session_id: &SessionId,
1344 ) -> Result<Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>, String> {
1345 let sessions = self.sessions.read().await;
1346 sessions
1347 .get(session_id)
1348 .map(|entry| Arc::clone(&entry.dsl_authority))
1349 .ok_or_else(|| {
1350 RuntimeDriverError::NotReady {
1351 state: RuntimeState::Destroyed,
1352 }
1353 .to_string()
1354 })
1355 }
1356
1357 #[cfg(any(test, feature = "test-support"))]
1358 async fn session_handle_teardown_gate(
1359 &self,
1360 session_id: &SessionId,
1361 ) -> Result<Arc<crate::handles::HandleTeardownGate>, String> {
1362 let sessions = self.sessions.read().await;
1363 sessions
1364 .get(session_id)
1365 .map(|entry| Arc::clone(&entry.handle_teardown_gate))
1366 .ok_or_else(|| {
1367 RuntimeDriverError::NotReady {
1368 state: RuntimeState::Destroyed,
1369 }
1370 .to_string()
1371 })
1372 }
1373
1374 #[cfg(any(test, feature = "test-support"))]
1382 pub async fn test_install_session_peer_comms_handle_on_runtime(
1383 &self,
1384 session_id: &SessionId,
1385 runtime: &(dyn meerkat_core::handles::PeerCommsInstallTarget + '_),
1386 ) -> Result<(), String> {
1387 let dsl = self
1388 .session_dsl_authority(session_id)
1389 .await
1390 .map_err(|error| format!("session dsl authority unavailable: {error}"))?;
1391 let teardown_gate = self
1392 .session_handle_teardown_gate(session_id)
1393 .await
1394 .map_err(|error| format!("session handle teardown gate unavailable: {error}"))?;
1395 let handle = std::sync::Arc::new(
1396 crate::handles::HandleDslAuthority::from_shared_with_teardown_gate(dsl, teardown_gate),
1397 );
1398 crate::handles::RuntimePeerCommsHandle::install_generated_on(handle, runtime)
1399 }
1400
1401 fn preview_dsl_input_on_state(
1402 state: &dsl::MeerkatMachineState,
1403 input: dsl::MeerkatMachineInput,
1404 context: &str,
1405 ) -> Result<Vec<dsl::MeerkatMachineEffect>, String> {
1406 let mut preview = dsl::MeerkatMachineAuthority::recover_from_state(state.clone())
1407 .map_err(|err| dsl_authority::map_error(err, context))?;
1408 dsl::MeerkatMachineMutator::apply(&mut preview, input)
1409 .map(|transition| transition.into_effects())
1410 .map_err(|err| dsl_authority::map_error(err, context))
1411 }
1412
1413 async fn preview_session_dsl_input(
1414 &self,
1415 session_id: &SessionId,
1416 input: dsl::MeerkatMachineInput,
1417 context: &str,
1418 ) -> Result<Vec<dsl::MeerkatMachineEffect>, String> {
1419 let authority = self.session_dsl_authority(session_id).await?;
1420 let state = {
1421 let authority = authority
1422 .lock()
1423 .unwrap_or_else(std::sync::PoisonError::into_inner);
1424 authority.state().clone()
1425 };
1426 Self::preview_dsl_input_on_state(&state, input, context)
1427 }
1428
1429 async fn session_dsl_state(
1430 &self,
1431 session_id: &SessionId,
1432 ) -> Result<dsl::MeerkatMachineState, RuntimeControlPlaneError> {
1433 let authority = self
1434 .session_dsl_authority(session_id)
1435 .await
1436 .map_err(RuntimeControlPlaneError::Internal)?;
1437 let authority = authority
1438 .lock()
1439 .unwrap_or_else(std::sync::PoisonError::into_inner);
1440 Ok(authority.state().clone())
1441 }
1442
1443 async fn commit_session_dsl_transition(
1444 &self,
1445 session_id: &SessionId,
1446 staged: StagedSessionDslInput,
1447 context: &str,
1448 ) -> Result<(), String> {
1449 self.commit_session_dsl_transition_with_dispatch_failure(
1450 session_id,
1451 staged,
1452 context,
1453 CommittedEffectDispatchFailure::PreserveCommittedDslState,
1454 )
1455 .await
1456 }
1457
1458 async fn commit_session_dsl_transition_preserving_committed_state(
1459 &self,
1460 session_id: &SessionId,
1461 staged: StagedSessionDslInput,
1462 context: &str,
1463 ) -> Result<(), String> {
1464 self.commit_session_dsl_transition_with_dispatch_failure(
1465 session_id,
1466 staged,
1467 context,
1468 CommittedEffectDispatchFailure::PreserveCommittedDslState,
1469 )
1470 .await
1471 }
1472
1473 async fn commit_session_dsl_transition_with_dispatch_failure(
1474 &self,
1475 _session_id: &SessionId,
1476 staged: StagedSessionDslInput,
1477 context: &str,
1478 dispatch_failure: CommittedEffectDispatchFailure,
1479 ) -> Result<(), String> {
1480 if let Err(error) = self
1481 .dispatch_routed_signals_from_effects(&staged.effects)
1482 .await
1483 {
1484 let CommittedEffectDispatchFailure::PreserveCommittedDslState = dispatch_failure;
1485 return Err(format!(
1486 "DSL authority ({context}): committed effect dispatch failed: {error}"
1487 ));
1488 }
1489 Ok(())
1490 }
1491
1492 async fn dispatch_routed_signals_from_effects(
1493 &self,
1494 effects: &[dsl::MeerkatMachineEffect],
1495 ) -> Result<(), String> {
1496 let dispatcher = {
1497 self.composition_signal_dispatcher
1498 .read()
1499 .unwrap_or_else(std::sync::PoisonError::into_inner)
1500 .clone()
1501 };
1502 let Some(dispatcher) = dispatcher else {
1503 return Ok(());
1504 };
1505
1506 for effect in effects {
1507 if let Some(signal) = composition::lift_routed_signal(effect) {
1508 composition::dispatch_routed_signal(&dispatcher, signal).await?;
1509 }
1510 }
1511 Ok(())
1512 }
1513
1514 async fn clear_dead_runtime_attachment(&self, session_id: &SessionId) {
1515 let mut sessions = self.sessions.write().await;
1516 if let Some(entry) = sessions.get_mut(session_id) {
1517 let cleared = entry.clear_dead_attachment();
1518 if cleared && let Err(error) = entry.stage_generated_executor_exit_observation() {
1519 tracing::warn!(
1520 %session_id,
1521 error = %error,
1522 "generated MeerkatMachine rejected executor-exit observation while clearing dead attachment"
1523 );
1524 }
1525 }
1526 }
1527
1528 async fn dispatch_cancel_after_boundary_runtime_effect(
1529 &self,
1530 session_id: &SessionId,
1531 effect_tx: Option<mpsc::Sender<crate::effect::RuntimeEffect>>,
1532 boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
1533 projected_effect: crate::effect::ProjectedRuntimeEffect,
1534 context: &str,
1535 ) -> Result<(), RuntimeDriverError> {
1536 let Some(effect_tx) = effect_tx else {
1537 let state = self
1538 .existing_session_runtime_state(session_id)
1539 .await
1540 .unwrap_or(RuntimeState::Destroyed);
1541 return Err(RuntimeDriverError::NotReady { state });
1542 };
1543
1544 let reason = projected_effect.reason().to_string();
1545 if let Some(boundary_handle) = boundary_handle {
1546 boundary_handle
1547 .cancel_after_boundary(reason)
1548 .await
1549 .map_err(|err| {
1550 RuntimeDriverError::Internal(format!(
1551 "{context}: failed to apply live boundary cancel: {err}"
1552 ))
1553 })?;
1554 }
1555
1556 match effect_tx.send(projected_effect.into_effect()).await {
1557 Ok(()) => Ok(()),
1558 Err(_) => {
1559 self.clear_dead_runtime_attachment(session_id).await;
1560 Err(RuntimeDriverError::NotReady {
1561 state: RuntimeState::Idle,
1562 })
1563 }
1564 }
1565 }
1566
1567 async fn restore_session_dsl_state(
1568 &self,
1569 session_id: &SessionId,
1570 snapshot: dsl::MeerkatMachineAuthoritySnapshot,
1571 ) {
1572 if let Ok(authority) = self.session_dsl_authority(session_id).await {
1573 Self::restore_dsl_authority_snapshot(&authority, snapshot);
1574 }
1575 }
1576
1577 async fn restore_session_dsl_state_if_current(
1578 &self,
1579 session_id: &SessionId,
1580 expected_current: dsl::MeerkatMachineAuthoritySnapshot,
1581 restore: dsl::MeerkatMachineAuthoritySnapshot,
1582 ) -> bool {
1583 let Ok(authority) = self.session_dsl_authority(session_id).await else {
1584 return false;
1585 };
1586 Self::restore_dsl_authority_snapshot_if_current(&authority, expected_current, restore)
1587 }
1588
1589 fn restore_dsl_authority_snapshot(
1590 authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1591 snapshot: dsl::MeerkatMachineAuthoritySnapshot,
1592 ) {
1593 let mut authority = authority
1594 .lock()
1595 .unwrap_or_else(std::sync::PoisonError::into_inner);
1596 authority.restore_snapshot(snapshot);
1597 }
1598
1599 fn restore_dsl_authority_snapshot_if_current(
1600 authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1601 expected_current: dsl::MeerkatMachineAuthoritySnapshot,
1602 restore: dsl::MeerkatMachineAuthoritySnapshot,
1603 ) -> bool {
1604 let mut authority = authority
1605 .lock()
1606 .unwrap_or_else(std::sync::PoisonError::into_inner);
1607 let current = authority.snapshot();
1608 if current.state() == expected_current.state() {
1609 authority.restore_snapshot(restore);
1610 true
1611 } else {
1612 false
1613 }
1614 }
1615}
1616
1617#[derive(Debug, Clone, Copy)]
1620pub struct MachineSessionControlAuthority {
1621 _private: (),
1622}
1623
1624#[cfg(feature = "live")]
1625struct LiveOpenAdmissionGeneratedAuthorityBridgeToken;
1626
1627#[cfg(feature = "live")]
1628struct LiveCloseResultGeneratedAuthorityBridgeToken;
1629
1630#[cfg(feature = "live")]
1631struct LiveChannelStatusResultGeneratedAuthorityBridgeToken;
1632
1633#[cfg(feature = "live")]
1634static LIVE_OPEN_ADMISSION_GENERATED_AUTHORITY_BRIDGE_TOKEN:
1635 LiveOpenAdmissionGeneratedAuthorityBridgeToken = LiveOpenAdmissionGeneratedAuthorityBridgeToken;
1636
1637#[cfg(feature = "live")]
1638static LIVE_CLOSE_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN:
1639 LiveCloseResultGeneratedAuthorityBridgeToken = LiveCloseResultGeneratedAuthorityBridgeToken;
1640
1641#[cfg(feature = "live")]
1642static LIVE_CHANNEL_STATUS_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN:
1643 LiveChannelStatusResultGeneratedAuthorityBridgeToken =
1644 LiveChannelStatusResultGeneratedAuthorityBridgeToken;
1645
1646#[cfg(feature = "live")]
1647fn live_open_admission_generated_authority_bridge_token()
1648-> &'static (dyn std::any::Any + Send + Sync) {
1649 &LIVE_OPEN_ADMISSION_GENERATED_AUTHORITY_BRIDGE_TOKEN
1650}
1651
1652#[cfg(feature = "live")]
1653fn live_close_result_generated_authority_bridge_token() -> &'static (dyn std::any::Any + Send + Sync)
1654{
1655 &LIVE_CLOSE_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN
1656}
1657
1658#[cfg(feature = "live")]
1659fn live_channel_status_result_generated_authority_bridge_token()
1660-> &'static (dyn std::any::Any + Send + Sync) {
1661 &LIVE_CHANNEL_STATUS_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN
1662}
1663
1664#[cfg(feature = "live")]
1665#[doc(hidden)]
1666#[allow(improper_ctypes_definitions, unsafe_code)]
1667#[unsafe(export_name = concat!(
1668 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_open_admission_",
1669 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1670))]
1671pub extern "Rust" fn live_open_admission_generated_authority_bridge_token_is_valid(
1672 token: &(dyn std::any::Any + Send + Sync),
1673) -> bool {
1674 token.is::<LiveOpenAdmissionGeneratedAuthorityBridgeToken>()
1675}
1676
1677#[cfg(feature = "live")]
1678#[doc(hidden)]
1679#[allow(improper_ctypes_definitions, unsafe_code)]
1680#[unsafe(export_name = concat!(
1681 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_close_result_",
1682 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1683))]
1684pub extern "Rust" fn live_close_result_generated_authority_bridge_token_is_valid(
1685 token: &(dyn std::any::Any + Send + Sync),
1686) -> bool {
1687 token.is::<LiveCloseResultGeneratedAuthorityBridgeToken>()
1688}
1689
1690#[cfg(feature = "live")]
1691#[doc(hidden)]
1692#[allow(improper_ctypes_definitions, unsafe_code)]
1693#[unsafe(export_name = concat!(
1694 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_channel_status_result_",
1695 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1696))]
1697pub extern "Rust" fn live_channel_status_result_generated_authority_bridge_token_is_valid(
1698 token: &(dyn std::any::Any + Send + Sync),
1699) -> bool {
1700 token.is::<LiveChannelStatusResultGeneratedAuthorityBridgeToken>()
1701}
1702
1703#[cfg(feature = "live")]
1704fn build_live_channel_open_authority(
1705 session_id: SessionId,
1706 channel_id: meerkat_live::LiveChannelId,
1707 sequence: u64,
1708) -> Result<meerkat_live::LiveChannelOpenAuthority, String> {
1709 #[allow(improper_ctypes_definitions, unsafe_code)]
1710 unsafe extern "Rust" {
1711 #[link_name = concat!(
1712 "__meerkat_live_runtime_generated_live_channel_open_authority_build_v1_",
1713 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1714 )]
1715 fn live_generated_channel_open_authority_build(
1716 token: &'static (dyn std::any::Any + Send + Sync),
1717 session_id: SessionId,
1718 channel_id: meerkat_live::LiveChannelId,
1719 sequence: u64,
1720 ) -> Result<meerkat_live::LiveChannelOpenAuthority, String>;
1721 }
1722 #[allow(unsafe_code)]
1723 unsafe {
1724 live_generated_channel_open_authority_build(
1725 live_open_admission_generated_authority_bridge_token(),
1726 session_id,
1727 channel_id,
1728 sequence,
1729 )
1730 }
1731}
1732
1733#[cfg(feature = "live")]
1734fn build_live_channel_close_commit_authority(
1735 channel_id: String,
1736 close_sequence: u64,
1737) -> Result<meerkat_live::LiveChannelCloseCommitAuthority, String> {
1738 #[allow(improper_ctypes_definitions, unsafe_code)]
1739 unsafe extern "Rust" {
1740 #[link_name = concat!(
1741 "__meerkat_live_runtime_generated_live_channel_close_commit_authority_build_v1_",
1742 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1743 )]
1744 fn live_generated_channel_close_commit_authority_build(
1745 token: &'static (dyn std::any::Any + Send + Sync),
1746 channel_id: String,
1747 close_sequence: u64,
1748 ) -> Result<meerkat_live::LiveChannelCloseCommitAuthority, String>;
1749 }
1750 #[allow(unsafe_code)]
1751 unsafe {
1752 live_generated_channel_close_commit_authority_build(
1753 live_close_result_generated_authority_bridge_token(),
1754 channel_id,
1755 close_sequence,
1756 )
1757 }
1758}
1759
1760#[cfg(feature = "live")]
1761fn build_live_channel_status_commit_authority(
1762 channel_id: String,
1763 status_observation_sequence: u64,
1764) -> Result<meerkat_live::LiveChannelStatusCommitAuthority, String> {
1765 #[allow(improper_ctypes_definitions, unsafe_code)]
1766 unsafe extern "Rust" {
1767 #[link_name = concat!(
1768 "__meerkat_live_runtime_generated_live_channel_status_commit_authority_build_v1_",
1769 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1770 )]
1771 fn live_generated_channel_status_commit_authority_build(
1772 token: &'static (dyn std::any::Any + Send + Sync),
1773 channel_id: String,
1774 status_observation_sequence: u64,
1775 ) -> Result<meerkat_live::LiveChannelStatusCommitAuthority, String>;
1776 }
1777 #[allow(unsafe_code)]
1778 unsafe {
1779 live_generated_channel_status_commit_authority_build(
1780 live_channel_status_result_generated_authority_bridge_token(),
1781 channel_id,
1782 status_observation_sequence,
1783 )
1784 }
1785}
1786
1787#[derive(Debug, Clone)]
1794#[cfg(feature = "live")]
1795pub struct LiveOpenAdmissionAuthority {
1796 session_id: SessionId,
1797 channel_id: meerkat_live::LiveChannelId,
1798 admitted: bool,
1799 rejection: Option<dsl::LiveOpenAdmissionRejection>,
1800 bound_llm_identity: Option<meerkat_core::SessionLlmIdentity>,
1801 sequence: u64,
1802 channel_open_authority: Option<meerkat_live::LiveChannelOpenAuthority>,
1803}
1804
1805#[cfg(feature = "live")]
1806impl LiveOpenAdmissionAuthority {
1807 pub(crate) fn from_generated_effect(
1808 session_id: SessionId,
1809 channel_id: meerkat_live::LiveChannelId,
1810 admitted: bool,
1811 rejection: Option<dsl::LiveOpenAdmissionRejection>,
1812 bound_llm_identity: Option<dsl::SessionLlmIdentity>,
1813 sequence: u64,
1814 ) -> Result<Self, String> {
1815 let bound_llm_identity = match (admitted, bound_llm_identity) {
1816 (true, Some(identity)) => Some(identity.try_into()?),
1817 (true, None) => {
1818 return Err(
1819 "generated live-open admission was admitted without bound LLM identity"
1820 .to_string(),
1821 );
1822 }
1823 (false, _) => None,
1824 };
1825 let channel_open_authority = if admitted {
1826 Some(build_live_channel_open_authority(
1827 session_id.clone(),
1828 channel_id.clone(),
1829 sequence,
1830 )?)
1831 } else {
1832 None
1833 };
1834 Ok(Self {
1835 session_id,
1836 channel_id,
1837 admitted,
1838 rejection,
1839 bound_llm_identity,
1840 sequence,
1841 channel_open_authority,
1842 })
1843 }
1844
1845 #[must_use]
1846 pub fn session_id(&self) -> &SessionId {
1847 &self.session_id
1848 }
1849
1850 #[must_use]
1851 pub fn channel_id(&self) -> &meerkat_live::LiveChannelId {
1852 &self.channel_id
1853 }
1854
1855 #[must_use]
1856 pub fn admitted(&self) -> bool {
1857 self.admitted
1858 }
1859
1860 #[must_use]
1861 pub fn rejection(&self) -> Option<dsl::LiveOpenAdmissionRejection> {
1862 self.rejection
1863 }
1864
1865 #[must_use]
1866 pub fn bound_llm_identity(&self) -> Option<&meerkat_core::SessionLlmIdentity> {
1867 self.bound_llm_identity.as_ref()
1868 }
1869
1870 #[must_use]
1871 pub fn sequence(&self) -> u64 {
1872 self.sequence
1873 }
1874
1875 #[must_use]
1876 pub fn channel_open_authority(&self) -> Option<&meerkat_live::LiveChannelOpenAuthority> {
1877 self.channel_open_authority.as_ref()
1878 }
1879}
1880
1881#[derive(Debug, Clone, PartialEq, Eq)]
1888#[cfg(feature = "live")]
1889pub struct LiveRefreshResultAuthority {
1890 pub status: dsl::LiveRefreshPublicStatus,
1891 pub sequence: u64,
1892 pub queue_acceptance_sequence: u64,
1893}
1894
1895#[derive(Debug, Clone)]
1900#[cfg(feature = "live")]
1901pub struct LiveCloseResultAuthority {
1902 pub status: dsl::LiveClosePublicStatus,
1903 pub sequence: u64,
1904 pub close_observation_sequence: u64,
1905 channel_close_commit_authority: Option<meerkat_live::LiveChannelCloseCommitAuthority>,
1906}
1907
1908#[cfg(feature = "live")]
1909impl LiveCloseResultAuthority {
1910 pub(crate) fn from_generated_effect(
1911 channel_id: String,
1912 status: dsl::LiveClosePublicStatus,
1913 sequence: u64,
1914 close_observation_sequence: u64,
1915 ) -> Result<Self, String> {
1916 let channel_close_commit_authority = match status {
1917 dsl::LiveClosePublicStatus::Closed => Some(build_live_channel_close_commit_authority(
1918 channel_id,
1919 close_observation_sequence,
1920 )?),
1921 };
1922 Ok(Self {
1923 status,
1924 sequence,
1925 close_observation_sequence,
1926 channel_close_commit_authority,
1927 })
1928 }
1929
1930 #[must_use]
1931 pub fn channel_close_commit_authority(
1932 &self,
1933 ) -> Option<&meerkat_live::LiveChannelCloseCommitAuthority> {
1934 self.channel_close_commit_authority.as_ref()
1935 }
1936
1937 #[must_use]
1938 pub fn into_channel_close_commit_authority(
1939 self,
1940 ) -> Option<meerkat_live::LiveChannelCloseCommitAuthority> {
1941 self.channel_close_commit_authority
1942 }
1943}
1944
1945#[derive(Debug, Clone, PartialEq, Eq)]
1951#[cfg(feature = "live")]
1952pub struct LiveCommandResultAuthority {
1953 pub command: dsl::LiveCommandPublicKind,
1954 pub sequence: u64,
1955 pub command_acceptance_sequence: u64,
1956}
1957
1958#[derive(Debug, Clone, PartialEq, Eq)]
1965#[cfg(feature = "live")]
1966pub struct LiveCommandRejectionAuthority {
1967 pub command: dsl::LiveCommandPublicKind,
1968 pub rejection: dsl::LiveCommandRejectionReason,
1969 pub public_error_class: dsl::LiveCommandRejectionPublicErrorClass,
1970 pub sequence: u64,
1971}
1972
1973#[derive(Debug, Clone, PartialEq, Eq)]
1980#[cfg(feature = "live")]
1981pub struct LiveChannelRequestRejectionAuthority {
1982 pub request: dsl::LiveChannelRequestPublicKind,
1983 pub rejection: dsl::LiveChannelRequestRejectionReason,
1984 pub public_error_class: dsl::LiveChannelRequestRejectionPublicErrorClass,
1985 pub sequence: u64,
1986}
1987
1988#[derive(Debug, Clone, PartialEq, Eq)]
1995#[cfg(feature = "live")]
1996pub struct LiveWebrtcTokenAuthority {
1997 pub token: String,
1998 pub expires_at_ms: u64,
1999 pub sequence: u64,
2000}
2001
2002#[derive(Debug, Clone, PartialEq, Eq)]
2008#[cfg(feature = "live")]
2009pub struct LiveWebrtcAnswerAdmissionAuthority {
2010 pub admitted: bool,
2011 pub rejection: Option<dsl::LiveWebrtcAnswerAdmissionRejection>,
2012 pub public_error_class: Option<dsl::LiveChannelRequestRejectionPublicErrorClass>,
2013 pub sequence: u64,
2014}
2015
2016#[derive(Debug, Clone, PartialEq, Eq)]
2023#[cfg(feature = "live")]
2024pub struct LiveWebrtcAnswerResultAuthority {
2025 pub status: dsl::LiveWebrtcAnswerPublicStatus,
2026 pub answered: bool,
2027 pub sequence: u64,
2028 pub answer_observation_sequence: u64,
2029}
2030
2031#[derive(Debug, Clone, PartialEq, Eq)]
2038#[cfg(feature = "live")]
2039pub struct LiveWebsocketTokenAuthority {
2040 pub token: String,
2041 pub expires_at_ms: u64,
2042 pub sequence: u64,
2043}
2044
2045#[derive(Debug, Clone, PartialEq, Eq)]
2051#[cfg(feature = "live")]
2052pub struct LiveWebsocketTokenAdmissionAuthority {
2053 pub admitted: bool,
2054 pub rejection: Option<dsl::LiveWebsocketTokenAdmissionRejection>,
2055 pub public_error_class: Option<dsl::LiveWebsocketTokenAdmissionPublicErrorClass>,
2056 pub sequence: u64,
2057}
2058
2059#[derive(Debug, Clone)]
2065#[cfg(feature = "live")]
2066pub struct LiveChannelStatusAuthority {
2067 pub status: dsl::LiveChannelPublicStatus,
2068 pub sequence: u64,
2069 pub status_observation_sequence: u64,
2070 pub degradation_reason: Option<dsl::LiveChannelDegradationReason>,
2071 pub degradation_detail: Option<String>,
2072 pub channel_status_commit_authority: Option<meerkat_live::LiveChannelStatusCommitAuthority>,
2073}
2074
2075#[cfg(feature = "live")]
2076impl LiveChannelStatusAuthority {
2077 pub(crate) fn from_generated_effect(
2078 channel_id: String,
2079 status: dsl::LiveChannelPublicStatus,
2080 sequence: u64,
2081 status_observation_sequence: u64,
2082 degradation_reason: Option<dsl::LiveChannelDegradationReason>,
2083 degradation_detail: Option<String>,
2084 ) -> Result<Self, String> {
2085 Ok(Self {
2086 status,
2087 sequence,
2088 status_observation_sequence,
2089 degradation_reason,
2090 degradation_detail,
2091 channel_status_commit_authority: Some(build_live_channel_status_commit_authority(
2092 channel_id,
2093 status_observation_sequence,
2094 )?),
2095 })
2096 }
2097
2098 #[must_use]
2099 pub fn channel_status_commit_authority(
2100 &self,
2101 ) -> Option<&meerkat_live::LiveChannelStatusCommitAuthority> {
2102 self.channel_status_commit_authority.as_ref()
2103 }
2104
2105 #[must_use]
2106 pub fn into_channel_status_commit_authority(
2107 self,
2108 ) -> Option<meerkat_live::LiveChannelStatusCommitAuthority> {
2109 self.channel_status_commit_authority
2110 }
2111}
2112
2113pub struct MeerkatMachine {
2120 sessions: RwLock<HashMap<SessionId, RuntimeSessionEntry>>,
2122 store: Option<Arc<dyn RuntimeStore>>,
2124 blob_store: Option<Arc<dyn BlobStore>>,
2126 llm_reconfigure_host: StdRwLock<Option<Arc<dyn SessionLlmReconfigureHost>>>,
2128 auth_lease: StdRwLock<meerkat_core::handles::GeneratedAuthLeaseHandle>,
2131 #[cfg(not(target_arch = "wasm32"))]
2134 oauth_flows: StdRwLock<Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority>>,
2135 #[cfg(feature = "live")]
2139 live_unbound_rejection_authority: crate::driver::ephemeral::SharedIngressDslAuthority,
2140 session_claims: Arc<crate::handles::RuntimeSessionClaimRegistry>,
2147 composition_signal_dispatcher:
2151 StdRwLock<Option<composition::MeerkatCompositionSignalDispatcher>>,
2152}
2153
2154impl MeerkatMachine {
2155 #[must_use]
2158 pub fn session_control_authority(&self) -> MachineSessionControlAuthority {
2159 MachineSessionControlAuthority { _private: () }
2160 }
2161
2162 #[must_use]
2167 pub fn shares_runtime_persistence_with(&self, other: &Self) -> bool {
2168 match (&self.store, &other.store) {
2169 (None, None) => true,
2170 (Some(a), Some(b)) => runtime_stores_share_authority(a, b),
2171 _ => false,
2172 }
2173 }
2174
2175 #[must_use]
2178 pub fn shares_runtime_store_authority(&self, store: &Arc<dyn RuntimeStore>) -> bool {
2179 self.store
2180 .as_ref()
2181 .is_some_and(|machine_store| runtime_stores_share_authority(machine_store, store))
2182 }
2183
2184 #[must_use]
2186 pub fn has_runtime_persistence(&self) -> bool {
2187 self.store.is_some()
2188 }
2189
2190 fn normalize_destroyed_error(err: RuntimeDriverError) -> RuntimeDriverError {
2191 match err {
2192 RuntimeDriverError::NotReady {
2193 state: RuntimeState::Destroyed,
2194 } => RuntimeDriverError::Destroyed,
2195 other => other,
2196 }
2197 }
2198
2199 pub fn ephemeral() -> Self {
2201 let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
2202 #[cfg(not(target_arch = "wasm32"))]
2203 let oauth_flows = Arc::new(crate::handles::RuntimeOAuthFlowHandle::new_with_auth_lease(
2204 std::time::Duration::from_secs(10 * 60),
2205 Arc::clone(&auth_lease),
2206 ));
2207 let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
2208 Self {
2209 sessions: RwLock::new(HashMap::new()),
2210 store: None,
2211 blob_store: None,
2212 llm_reconfigure_host: StdRwLock::new(None),
2213 auth_lease: StdRwLock::new(auth_lease),
2214 #[cfg(not(target_arch = "wasm32"))]
2215 oauth_flows: StdRwLock::new(oauth_flows),
2216 #[cfg(feature = "live")]
2217 live_unbound_rejection_authority: live_unbound_rejection_authority(),
2218 session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
2219 composition_signal_dispatcher: StdRwLock::new(None),
2220 }
2221 }
2222
2223 pub fn persistent(store: Arc<dyn RuntimeStore>, blob_store: Arc<dyn BlobStore>) -> Self {
2225 #[cfg(not(target_arch = "wasm32"))]
2226 let (auth_lease, oauth_flows) = {
2227 let authorities = persistent_auth_authorities(&store);
2228 (
2229 Arc::clone(&authorities.auth_lease),
2230 Arc::clone(&authorities.oauth_flows),
2231 )
2232 };
2233 #[cfg(target_arch = "wasm32")]
2234 let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
2235 let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
2236 Self {
2237 sessions: RwLock::new(HashMap::new()),
2238 store: Some(store),
2239 blob_store: Some(blob_store),
2240 llm_reconfigure_host: StdRwLock::new(None),
2241 auth_lease: StdRwLock::new(auth_lease),
2242 #[cfg(not(target_arch = "wasm32"))]
2243 oauth_flows: StdRwLock::new(oauth_flows),
2244 #[cfg(feature = "live")]
2245 live_unbound_rejection_authority: live_unbound_rejection_authority(),
2246 session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
2247 composition_signal_dispatcher: StdRwLock::new(None),
2248 }
2249 }
2250
2251 pub fn persistent_without_blobs(store: Arc<dyn RuntimeStore>) -> Self {
2257 #[cfg(not(target_arch = "wasm32"))]
2258 let (auth_lease, oauth_flows) = {
2259 let authorities = persistent_auth_authorities(&store);
2260 (
2261 Arc::clone(&authorities.auth_lease),
2262 Arc::clone(&authorities.oauth_flows),
2263 )
2264 };
2265 #[cfg(target_arch = "wasm32")]
2266 let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
2267 let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
2268 Self {
2269 sessions: RwLock::new(HashMap::new()),
2270 store: Some(store),
2271 blob_store: Some(Arc::new(UnavailableBlobStore)),
2272 llm_reconfigure_host: StdRwLock::new(None),
2273 auth_lease: StdRwLock::new(auth_lease),
2274 #[cfg(not(target_arch = "wasm32"))]
2275 oauth_flows: StdRwLock::new(oauth_flows),
2276 #[cfg(feature = "live")]
2277 live_unbound_rejection_authority: live_unbound_rejection_authority(),
2278 session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
2279 composition_signal_dispatcher: StdRwLock::new(None),
2280 }
2281 }
2282
2283 pub fn auth_lease_handle(&self) -> Arc<dyn meerkat_core::handles::AuthLeaseHandle> {
2286 self.generated_auth_lease_handle().clone_handle()
2287 }
2288
2289 pub fn generated_auth_lease_handle(&self) -> meerkat_core::handles::GeneratedAuthLeaseHandle {
2292 self.auth_lease
2293 .read()
2294 .unwrap_or_else(std::sync::PoisonError::into_inner)
2295 .clone()
2296 }
2297
2298 pub fn set_auth_lease_handle(&self, handle: Arc<crate::handles::RuntimeAuthLeaseHandle>) {
2304 self.set_runtime_auth_lease_handle(handle);
2305 }
2306
2307 #[cfg(not(target_arch = "wasm32"))]
2313 pub fn set_auth_lease_handle_with_oauth_flow_authority(
2314 &self,
2315 handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
2316 oauth_flows: Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority>,
2317 ) {
2318 *self
2319 .oauth_flows
2320 .write()
2321 .unwrap_or_else(std::sync::PoisonError::into_inner) = oauth_flows;
2322 let handle = generated_runtime_auth_lease_handle(handle);
2323 *self
2324 .auth_lease
2325 .write()
2326 .unwrap_or_else(std::sync::PoisonError::into_inner) = handle;
2327 }
2328
2329 pub fn set_runtime_auth_lease_handle(
2332 &self,
2333 handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
2334 ) {
2335 #[cfg(not(target_arch = "wasm32"))]
2336 {
2337 *self
2338 .oauth_flows
2339 .write()
2340 .unwrap_or_else(std::sync::PoisonError::into_inner) =
2341 Arc::new(crate::handles::RuntimeOAuthFlowHandle::new_with_auth_lease(
2342 std::time::Duration::from_secs(10 * 60),
2343 Arc::clone(&handle),
2344 ));
2345 }
2346 let handle = generated_runtime_auth_lease_handle(handle);
2347 *self
2348 .auth_lease
2349 .write()
2350 .unwrap_or_else(std::sync::PoisonError::into_inner) = handle;
2351 }
2352
2353 #[cfg(not(target_arch = "wasm32"))]
2356 pub fn oauth_flow_authority(
2357 &self,
2358 ) -> Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority> {
2359 Arc::clone(
2360 &self
2361 .oauth_flows
2362 .read()
2363 .unwrap_or_else(std::sync::PoisonError::into_inner),
2364 )
2365 }
2366
2367 pub fn session_claim_handle(&self) -> Arc<dyn meerkat_core::handles::SessionClaimHandle> {
2372 Arc::clone(&self.session_claims) as Arc<dyn meerkat_core::handles::SessionClaimHandle>
2373 }
2374
2375 pub fn set_composition_signal_dispatcher(
2378 &self,
2379 dispatcher: composition::MeerkatCompositionSignalDispatcher,
2380 ) {
2381 let mut slot = self
2382 .composition_signal_dispatcher
2383 .write()
2384 .unwrap_or_else(std::sync::PoisonError::into_inner);
2385 *slot = Some(dispatcher);
2386 }
2387
2388 pub(crate) async fn apply_routed_meerkat_input(
2399 &self,
2400 session_id: &SessionId,
2401 input: dsl::MeerkatMachineInput,
2402 ) -> Result<(), dsl_authority::DslTransitionRefusal> {
2403 let _gate_guard = self
2404 .lock_current_session_mutation_gate(session_id)
2405 .await
2406 .ok_or_else(|| {
2407 dsl_authority::DslTransitionRefusal::other(
2408 "routed_session_not_registered",
2409 format!(
2410 "session `{session_id}` is not registered with this MeerkatMachine; \
2411 cannot deliver routed input"
2412 ),
2413 )
2414 })?;
2415 self.apply_routed_session_dsl_input(session_id, input, "RoutedMeerkatInput")
2416 .await
2417 .map(|_| ())
2418 }
2419
2420 #[cfg(test)]
2421 pub(crate) async fn debug_shared_ingress_authorities(
2422 &self,
2423 session_id: &SessionId,
2424 ) -> Option<(
2425 Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
2426 crate::driver::ephemeral::SharedIngressDslAuthority,
2427 )> {
2428 let sessions = self.sessions.read().await;
2429 let entry = sessions.get(session_id)?;
2430 let session_authority = Arc::clone(&entry.dsl_authority);
2431 let driver = entry.driver.lock().await;
2432 Some((session_authority, driver.shared_dsl_authority()))
2433 }
2434
2435 fn make_driver(
2437 &self,
2438 runtime_id: LogicalRuntimeId,
2439 dsl_authority: crate::driver::ephemeral::SharedIngressDslAuthority,
2440 initial_runtime_state: RuntimeState,
2441 ) -> DriverEntry {
2442 let control_projection = Arc::new(StdRwLock::new(
2443 crate::driver::ephemeral::RuntimeControlProjection {
2444 phase: initial_runtime_state,
2445 current_run_id: None,
2446 pre_run_phase: None,
2447 },
2448 ));
2449 match (&self.store, &self.blob_store) {
2450 (Some(store), Some(blob_store)) => {
2451 DriverEntry::Persistent(PersistentRuntimeDriver::new_with_control(
2452 runtime_id,
2453 store.clone(),
2454 blob_store.clone(),
2455 control_projection,
2456 dsl_authority,
2457 ))
2458 }
2459 _ => DriverEntry::Ephemeral(EphemeralRuntimeDriver::new_with_control_and_dsl(
2460 runtime_id,
2461 control_projection,
2462 dsl_authority,
2463 )),
2464 }
2465 }
2466
2467 async fn recover_or_create_ops_state(
2474 &self,
2475 session_id: &SessionId,
2476 runtime_id: &LogicalRuntimeId,
2477 ) -> Result<
2478 (
2479 Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
2480 meerkat_core::RuntimeEpochId,
2481 Arc<meerkat_core::EpochCursorState>,
2482 ),
2483 RuntimeDriverError,
2484 > {
2485 if let Some(ref store) = self.store {
2486 match store.load_ops_lifecycle(runtime_id).await {
2487 Ok(Some(snapshot)) => {
2488 let recovered_epoch = snapshot.epoch_id.clone();
2489 let recovered_ops_count = snapshot.completion_entries.len();
2490 let registry =
2491 match crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::from_recovered(
2492 snapshot,
2493 ) {
2494 Ok(registry) => registry,
2495 Err(err) => {
2496 tracing::error!(
2497 %session_id,
2498 %runtime_id,
2499 error = %err,
2500 "failed to recover ops lifecycle through generated authority"
2501 );
2502 return Err(RuntimeDriverError::Internal(format!(
2503 "failed to recover ops lifecycle through generated authority: {err}"
2504 )));
2505 }
2506 };
2507 let recovered_cursor_snapshot = registry.completion_cursor_snapshot();
2508 let recovered_cursors = meerkat_core::EpochCursorState::from_recovered(
2509 recovered_cursor_snapshot.agent_applied_cursor,
2510 recovered_cursor_snapshot.runtime_observed_seq,
2511 recovered_cursor_snapshot.runtime_last_injected_seq,
2512 );
2513 tracing::info!(
2514 %session_id,
2515 %runtime_id,
2516 epoch_id = %recovered_epoch,
2517 recovered_ops = recovered_ops_count,
2518 "ops lifecycle recovered from durable store (same epoch)"
2519 );
2520 return Ok((
2521 Arc::new(registry),
2522 recovered_epoch,
2523 Arc::new(recovered_cursors),
2524 ));
2525 }
2526 Ok(None) => {}
2527 Err(err) => {
2528 tracing::error!(
2529 %session_id,
2530 %runtime_id,
2531 error = %err,
2532 "failed to load ops lifecycle from durable store"
2533 );
2534 return Err(RuntimeDriverError::Internal(format!(
2535 "failed to load ops lifecycle from durable store: {err}"
2536 )));
2537 }
2538 }
2539 tracing::debug!(%session_id, "no persisted ops lifecycle; fresh epoch");
2540 Ok((
2541 Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new()),
2542 meerkat_core::RuntimeEpochId::new(),
2543 Arc::new(meerkat_core::EpochCursorState::new()),
2544 ))
2545 } else {
2546 Ok((
2547 Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new()),
2548 meerkat_core::RuntimeEpochId::new(),
2549 Arc::new(meerkat_core::EpochCursorState::new()),
2550 ))
2551 }
2552 }
2553
2554 fn fresh_ops_state() -> (
2555 Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
2556 meerkat_core::RuntimeEpochId,
2557 Arc<meerkat_core::EpochCursorState>,
2558 ) {
2559 let registry = Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new());
2560 let epoch = meerkat_core::RuntimeEpochId::new();
2561 let cursors = Arc::new(meerkat_core::EpochCursorState::new());
2562 (registry, epoch, cursors)
2563 }
2564
2565 #[allow(clippy::large_futures)]
2566 fn execute_meerkat_machine_command(
2567 &self,
2568 self_handle: Option<Arc<Self>>,
2569 command: MeerkatMachineCommand,
2570 ) -> MeerkatMachineCommandFuture<'_> {
2571 Box::pin(async move {
2572 match command {
2573 MeerkatMachineCommand::EnsureSessionWithExecutor { .. } => {
2574 let self_handle = self_handle.ok_or_else(|| {
2575 MeerkatMachineCommandError::Driver(RuntimeDriverError::Internal(
2576 "EnsureSessionWithExecutor requires Arc<Self> machine handle".into(),
2577 ))
2578 })?;
2579 self_handle
2580 .execute_meerkat_machine_ensure_session_command(command)
2581 .await
2582 .map_err(Into::into)
2583 }
2584 MeerkatMachineCommand::RegisterSession { .. }
2585 | MeerkatMachineCommand::UnregisterSession { .. }
2586 | MeerkatMachineCommand::SetSilentIntents { .. }
2587 | MeerkatMachineCommand::CancelAfterBoundary { .. }
2588 | MeerkatMachineCommand::StopRuntimeExecutor { .. }
2589 | MeerkatMachineCommand::CommitServiceTurnTerminalReceipt { .. }
2590 | MeerkatMachineCommand::ContainsSession { .. }
2591 | MeerkatMachineCommand::SessionHasExecutor { .. }
2592 | MeerkatMachineCommand::SessionHasComms { .. }
2593 | MeerkatMachineCommand::OpsLifecycleRegistry { .. }
2594 | MeerkatMachineCommand::PrepareBindings { .. }
2595 | MeerkatMachineCommand::PrepareLocalSessionBindings { .. }
2596 | MeerkatMachineCommand::InputState { .. }
2597 | MeerkatMachineCommand::InputStateByIdempotencyKey { .. }
2598 | MeerkatMachineCommand::ListActiveInputs { .. }
2599 | MeerkatMachineCommand::ReconfigureSessionLlmIdentity { .. }
2600 | MeerkatMachineCommand::StagePersistentFilter { .. }
2601 | MeerkatMachineCommand::RequestDeferredTools { .. }
2602 | MeerkatMachineCommand::PublishCommittedVisibleSet { .. } => self
2603 .execute_meerkat_machine_session_command(command)
2604 .await
2605 .map_err(Into::into),
2606 MeerkatMachineCommand::SetPeerIngressContext { .. }
2607 | MeerkatMachineCommand::NotifyDrainExited { .. } => {
2608 let self_handle = self_handle.ok_or_else(|| {
2609 MeerkatMachineCommandError::Driver(RuntimeDriverError::Internal(
2610 "drain command requires Arc<Self> machine handle".into(),
2611 ))
2612 })?;
2613 self_handle
2614 .execute_meerkat_machine_drain_command(command)
2615 .await
2616 .map_err(Into::into)
2617 }
2618 MeerkatMachineCommand::AbortAll
2619 | MeerkatMachineCommand::Abort { .. }
2620 | MeerkatMachineCommand::Wait { .. } => self
2621 .execute_meerkat_machine_drain_local_command(command)
2622 .await
2623 .map_err(Into::into),
2624 MeerkatMachineCommand::Ingest { .. }
2625 | MeerkatMachineCommand::PublishEvent { .. }
2626 | MeerkatMachineCommand::Retire { .. }
2627 | MeerkatMachineCommand::Recycle { .. }
2628 | MeerkatMachineCommand::Reset { .. }
2629 | MeerkatMachineCommand::Recover { .. }
2630 | MeerkatMachineCommand::Destroy { .. }
2631 | MeerkatMachineCommand::RuntimeState { .. }
2632 | MeerkatMachineCommand::ResolvedSessionLlmCapabilities { .. }
2633 | MeerkatMachineCommand::ConfigureModelRoutingBaseline { .. }
2634 | MeerkatMachineCommand::SessionModelRoutingStatus { .. }
2635 | MeerkatMachineCommand::RequestSwitchTurn { .. }
2636 | MeerkatMachineCommand::AdmitModelRoutingAssistantTurn { .. }
2637 | MeerkatMachineCommand::BeginImageOperation { .. }
2638 | MeerkatMachineCommand::DenyImageOperationPlan { .. }
2639 | MeerkatMachineCommand::ActivateImageOperationOverride { .. }
2640 | MeerkatMachineCommand::ClassifyImageOperationTerminal { .. }
2641 | MeerkatMachineCommand::CompleteImageOperation { .. }
2642 | MeerkatMachineCommand::RestoreImageOperationOverride { .. }
2643 | MeerkatMachineCommand::LoadBoundaryReceipt { .. } => self
2644 .execute_meerkat_machine_control_command(command)
2645 .await
2646 .map_err(Into::into),
2647 MeerkatMachineCommand::AcceptWithCompletion { .. }
2648 | MeerkatMachineCommand::AcceptWithoutWake { .. } => self
2649 .execute_meerkat_machine_ingress_command(command)
2650 .await
2651 .map_err(Into::into),
2652 }
2653 })
2654 }
2655
2656 pub async fn register_session(
2663 &self,
2664 session_id: SessionId,
2665 ) -> Result<(), RuntimeControlPlaneError> {
2666 match self
2667 .execute_meerkat_machine_command(
2668 None,
2669 MeerkatMachineCommand::RegisterSession { session_id },
2670 )
2671 .await
2672 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
2673 {
2674 MeerkatMachineCommandResult::Unit => Ok(()),
2675 other => Err(RuntimeControlPlaneError::Internal(format!(
2676 "register_session: unexpected command result variant: {other:?}"
2677 ))),
2678 }
2679 }
2680}
2681
2682#[cfg(test)]
2683#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2684#[path = "../meerkat_machine_tests.rs"]
2685mod tests;