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, GeneratedSupervisorBinding, GeneratedSupervisorRotationReceipt,
932 GeneratedSupervisorRotationSubmit, SupervisorAuthorizeAdmission, SupervisorBindAdmission,
933 SupervisorBridgeCommandAdmission, SupervisorRotationObservation, SupervisorRotationSubmission,
934 SupervisorRotationTaskSlot,
935};
936pub(crate) use dsl_effects::{DslTransitionEffects, apply_dsl_transition_on_authority};
937pub(crate) use visibility::MachineToolVisibilityOwner;
938
939struct StagedSessionDslInput {
940 previous_snapshot: dsl::MeerkatMachineAuthoritySnapshot,
941 committed_snapshot: dsl::MeerkatMachineAuthoritySnapshot,
942 effects: DslTransitionEffects,
943}
944
945impl StagedSessionDslInput {
946 fn revived_stopped_session(&self) -> bool {
953 self.effects.as_slice().iter().any(|effect| {
954 matches!(
955 effect,
956 dsl::MeerkatMachineEffect::RuntimeNotice {
957 kind: dsl::RuntimeNoticeKind::Recover,
958 ..
959 }
960 )
961 })
962 }
963}
964
965#[derive(Clone, Copy)]
966enum CommittedEffectDispatchFailure {
967 PreserveCommittedDslState,
968}
969
970struct RuntimeSessionEntry {
972 runtime_id: LogicalRuntimeId,
974 mutation_gate: Arc<Mutex<()>>,
986 supervisor_rotation_task: Arc<SupervisorRotationTaskSlot>,
989 driver: SharedDriver,
991 control_projection: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
997 ops_lifecycle: Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
999 epoch_id: meerkat_core::RuntimeEpochId,
1001 handle_teardown_gate: Arc<crate::handles::HandleTeardownGate>,
1006 cursor_state: Arc<meerkat_core::EpochCursorState>,
1008 completions: SharedCompletionRegistry,
1010 tool_visibility_owner: Arc<MachineToolVisibilityOwner>,
1012 attachment_slot: RuntimeLoopAttachmentSlot,
1017 provisional_interrupt_handle:
1020 Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
1021 dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1031 drain_slot: CommsDrainSlot,
1041}
1042
1043struct RuntimeLoopAttachment {
1048 wake_tx: mpsc::Sender<()>,
1049 effect_tx: mpsc::Sender<crate::effect::RuntimeEffect>,
1050 boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
1051 interrupt_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
1052 loop_handle: tokio::task::JoinHandle<()>,
1053}
1054
1055enum RuntimeLoopAttachmentSlot {
1057 Empty,
1058 Attached(RuntimeLoopAttachment),
1059}
1060
1061impl RuntimeSessionEntry {
1062 fn control_snapshot(&self) -> crate::driver::ephemeral::RuntimeControlProjection {
1063 self.control_projection
1064 .read()
1065 .map(|guard| guard.clone())
1066 .unwrap_or_else(|poisoned| {
1067 tracing::error!("runtime control projection lock poisoned");
1068 poisoned.into_inner().clone()
1069 })
1070 }
1071
1072 fn attachment_is_live(&self) -> bool {
1073 match &self.attachment_slot {
1074 RuntimeLoopAttachmentSlot::Attached(attachment) => {
1075 !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed()
1076 }
1077 RuntimeLoopAttachmentSlot::Empty => false,
1078 }
1079 }
1080
1081 fn generated_executor_registration_active(&self) -> bool {
1082 let authority = self
1083 .dsl_authority
1084 .lock()
1085 .unwrap_or_else(std::sync::PoisonError::into_inner);
1086 matches!(
1087 authority.state().registration_phase,
1088 dsl::RegistrationPhase::Active
1089 )
1090 }
1091
1092 fn close_handle_teardown_gate(&self) {
1093 let _guard = self
1094 .dsl_authority
1095 .lock()
1096 .unwrap_or_else(std::sync::PoisonError::into_inner);
1097 self.handle_teardown_gate.close();
1098 }
1099
1100 fn generated_executor_registration_active_or_draining(&self) -> bool {
1108 let authority = self
1109 .dsl_authority
1110 .lock()
1111 .unwrap_or_else(std::sync::PoisonError::into_inner);
1112 matches!(
1113 authority.state().registration_phase,
1114 dsl::RegistrationPhase::Active | dsl::RegistrationPhase::Draining
1115 )
1116 }
1117
1118 fn generated_stop_deferred(&self) -> bool {
1119 self.dsl_authority
1120 .lock()
1121 .unwrap_or_else(std::sync::PoisonError::into_inner)
1122 .state()
1123 .runtime_stop_deferred
1124 }
1125
1126 fn stage_generated_executor_registration_claim(
1127 &self,
1128 session_id: &SessionId,
1129 ) -> Result<StagedSessionDslInput, String> {
1130 let staged = MeerkatMachine::stage_dsl_transition_on_authority(
1131 &self.dsl_authority,
1132 dsl::MeerkatMachineInput::EnsureSessionWithExecutor {
1133 session_id: dsl::SessionId::from_domain(session_id),
1134 },
1135 "EnsureSessionWithExecutor",
1136 )?;
1137 if self.generated_executor_registration_active() {
1138 Ok(staged)
1139 } else {
1140 let mut authority = self
1141 .dsl_authority
1142 .lock()
1143 .unwrap_or_else(std::sync::PoisonError::into_inner);
1144 authority.restore_snapshot(staged.previous_snapshot);
1145 Err("generated MeerkatMachine did not grant active executor registration".into())
1146 }
1147 }
1148
1149 fn stage_generated_executor_exit_observation(&self) -> Result<StagedSessionDslInput, String> {
1150 MeerkatMachine::stage_runtime_internal_dsl_transition_on_authority(
1151 &self.dsl_authority,
1152 crate::meerkat_machine_types::MeerkatMachineFieldlessRuntimeInternalInput::RuntimeExecutorExited,
1153 )
1154 }
1155
1156 fn has_live_attachment(&self) -> bool {
1159 self.attachment_is_live()
1160 }
1161
1162 fn attach_runtime_loop(
1163 &mut self,
1164 wake_tx: mpsc::Sender<()>,
1165 effect_tx: mpsc::Sender<crate::effect::RuntimeEffect>,
1166 boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
1167 interrupt_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
1168 loop_handle: tokio::task::JoinHandle<()>,
1169 ) {
1170 self.provisional_interrupt_handle = None;
1171 self.attachment_slot = RuntimeLoopAttachmentSlot::Attached(RuntimeLoopAttachment {
1172 wake_tx,
1173 effect_tx,
1174 boundary_handle,
1175 interrupt_handle,
1176 loop_handle,
1177 });
1178 }
1179
1180 fn take_loop_join_handle(&mut self) -> Option<tokio::task::JoinHandle<()>> {
1188 match std::mem::replace(&mut self.attachment_slot, RuntimeLoopAttachmentSlot::Empty) {
1189 RuntimeLoopAttachmentSlot::Attached(attachment) => Some(attachment.loop_handle),
1190 RuntimeLoopAttachmentSlot::Empty => None,
1191 }
1192 }
1193
1194 fn clear_dead_attachment(&mut self) -> bool {
1195 if matches!(self.attachment_slot, RuntimeLoopAttachmentSlot::Attached(_))
1196 && !self.attachment_is_live()
1197 {
1198 self.attachment_slot = RuntimeLoopAttachmentSlot::Empty;
1199 return true;
1200 }
1201 false
1202 }
1203
1204 fn wake_sender(&self) -> Option<mpsc::Sender<()>> {
1205 match &self.attachment_slot {
1206 RuntimeLoopAttachmentSlot::Attached(attachment)
1207 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1208 {
1209 Some(attachment.wake_tx.clone())
1210 }
1211 _ => None,
1212 }
1213 }
1214
1215 fn effect_sender(&self) -> Option<mpsc::Sender<crate::effect::RuntimeEffect>> {
1216 match &self.attachment_slot {
1217 RuntimeLoopAttachmentSlot::Attached(attachment)
1218 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1219 {
1220 Some(attachment.effect_tx.clone())
1221 }
1222 _ => None,
1223 }
1224 }
1225
1226 fn boundary_handle(
1227 &self,
1228 ) -> Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>> {
1229 match &self.attachment_slot {
1230 RuntimeLoopAttachmentSlot::Attached(attachment)
1231 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1232 {
1233 attachment.boundary_handle.clone()
1234 }
1235 _ => None,
1236 }
1237 }
1238
1239 fn interrupt_handle(
1240 &self,
1241 ) -> Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>> {
1242 match &self.attachment_slot {
1243 RuntimeLoopAttachmentSlot::Attached(attachment)
1244 if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
1245 {
1246 attachment.interrupt_handle.clone()
1247 }
1248 _ => self.provisional_interrupt_handle.clone(),
1249 }
1250 }
1251
1252 fn install_provisional_interrupt_handle(
1253 &mut self,
1254 handle: Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>,
1255 ) {
1256 if !self.attachment_is_live() {
1257 self.provisional_interrupt_handle = Some(handle);
1258 }
1259 }
1260}
1261
1262impl MeerkatMachine {
1263 async fn session_mutation_gate(&self, session_id: &SessionId) -> Option<Arc<Mutex<()>>> {
1269 let sessions = self.sessions.read().await;
1270 sessions
1271 .get(session_id)
1272 .map(|entry| Arc::clone(&entry.mutation_gate))
1273 }
1274
1275 async fn lock_current_session_mutation_gate(
1276 &self,
1277 session_id: &SessionId,
1278 ) -> Option<crate::tokio::sync::OwnedMutexGuard<()>> {
1279 loop {
1280 let gate = self.session_mutation_gate(session_id).await?;
1281 let gate_guard = Arc::clone(&gate).lock_owned().await;
1282 let sessions = self.sessions.read().await;
1283 let entry = sessions.get(session_id)?;
1284 if Arc::ptr_eq(&entry.mutation_gate, &gate) {
1285 return Some(gate_guard);
1286 }
1287 }
1288 }
1289
1290 pub(crate) async fn lock_current_session_driver_gate(
1291 &self,
1292 session_id: &SessionId,
1293 driver: &SharedDriver,
1294 ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
1295 let gate_guard = self
1296 .lock_current_session_mutation_gate(session_id)
1297 .await
1298 .ok_or(RuntimeDriverError::NotReady {
1299 state: RuntimeState::Destroyed,
1300 })?;
1301 {
1302 let sessions = self.sessions.read().await;
1303 let entry = sessions
1304 .get(session_id)
1305 .ok_or(RuntimeDriverError::NotReady {
1306 state: RuntimeState::Destroyed,
1307 })?;
1308 if !Arc::ptr_eq(&entry.driver, driver) {
1309 return Err(RuntimeDriverError::NotReady {
1310 state: RuntimeState::Destroyed,
1311 });
1312 }
1313 }
1314 Ok(gate_guard)
1315 }
1316
1317 pub(crate) async fn lock_current_runtime_loop_driver_authority(
1318 &self,
1319 session_id: &SessionId,
1320 driver: &SharedDriver,
1321 ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
1322 let gate_guard = self
1323 .lock_current_session_driver_gate(session_id, driver)
1324 .await?;
1325 {
1326 let sessions = self.sessions.read().await;
1327 let entry = sessions
1328 .get(session_id)
1329 .ok_or(RuntimeDriverError::NotReady {
1330 state: RuntimeState::Destroyed,
1331 })?;
1332 if !entry.generated_executor_registration_active_or_draining() {
1333 return Err(RuntimeDriverError::ValidationFailed {
1334 reason:
1335 "generated MeerkatMachine has no active runtime-loop executor registration"
1336 .to_string(),
1337 });
1338 }
1339 }
1340 Ok(gate_guard)
1341 }
1342
1343 async fn current_session_driver_with_authority(
1344 &self,
1345 session_id: &SessionId,
1346 ) -> Result<(SharedDriver, crate::tokio::sync::OwnedMutexGuard<()>), RuntimeDriverError> {
1347 let gate_guard = self
1348 .lock_current_session_mutation_gate(session_id)
1349 .await
1350 .ok_or(RuntimeDriverError::NotReady {
1351 state: RuntimeState::Destroyed,
1352 })?;
1353 let driver = {
1354 let sessions = self.sessions.read().await;
1355 sessions
1356 .get(session_id)
1357 .ok_or(RuntimeDriverError::NotReady {
1358 state: RuntimeState::Destroyed,
1359 })?
1360 .driver
1361 .clone()
1362 };
1363 Ok((driver, gate_guard))
1364 }
1365
1366 async fn session_dsl_authority(
1367 &self,
1368 session_id: &SessionId,
1369 ) -> Result<Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>, String> {
1370 let sessions = self.sessions.read().await;
1371 sessions
1372 .get(session_id)
1373 .map(|entry| Arc::clone(&entry.dsl_authority))
1374 .ok_or_else(|| {
1375 RuntimeDriverError::NotReady {
1376 state: RuntimeState::Destroyed,
1377 }
1378 .to_string()
1379 })
1380 }
1381
1382 #[cfg(any(test, feature = "test-support"))]
1383 async fn session_handle_teardown_gate(
1384 &self,
1385 session_id: &SessionId,
1386 ) -> Result<Arc<crate::handles::HandleTeardownGate>, String> {
1387 let sessions = self.sessions.read().await;
1388 sessions
1389 .get(session_id)
1390 .map(|entry| Arc::clone(&entry.handle_teardown_gate))
1391 .ok_or_else(|| {
1392 RuntimeDriverError::NotReady {
1393 state: RuntimeState::Destroyed,
1394 }
1395 .to_string()
1396 })
1397 }
1398
1399 #[cfg(any(test, feature = "test-support"))]
1407 pub async fn test_install_session_peer_comms_handle_on_runtime(
1408 &self,
1409 session_id: &SessionId,
1410 runtime: &(dyn meerkat_core::handles::PeerCommsInstallTarget + '_),
1411 ) -> Result<(), String> {
1412 let dsl = self
1413 .session_dsl_authority(session_id)
1414 .await
1415 .map_err(|error| format!("session dsl authority unavailable: {error}"))?;
1416 let teardown_gate = self
1417 .session_handle_teardown_gate(session_id)
1418 .await
1419 .map_err(|error| format!("session handle teardown gate unavailable: {error}"))?;
1420 let handle = std::sync::Arc::new(
1421 crate::handles::HandleDslAuthority::from_shared_with_teardown_gate(dsl, teardown_gate),
1422 );
1423 crate::handles::RuntimePeerCommsHandle::install_generated_on(handle, runtime)
1424 }
1425
1426 fn preview_dsl_input_on_state(
1427 state: &dsl::MeerkatMachineState,
1428 input: dsl::MeerkatMachineInput,
1429 context: &str,
1430 ) -> Result<Vec<dsl::MeerkatMachineEffect>, String> {
1431 let mut preview = dsl::MeerkatMachineAuthority::recover_from_state(state.clone())
1432 .map_err(|err| dsl_authority::map_error(err, context))?;
1433 dsl::MeerkatMachineMutator::apply(&mut preview, input)
1434 .map(|transition| transition.into_effects())
1435 .map_err(|err| dsl_authority::map_error(err, context))
1436 }
1437
1438 async fn preview_session_dsl_input(
1439 &self,
1440 session_id: &SessionId,
1441 input: dsl::MeerkatMachineInput,
1442 context: &str,
1443 ) -> Result<Vec<dsl::MeerkatMachineEffect>, String> {
1444 let authority = self.session_dsl_authority(session_id).await?;
1445 let state = {
1446 let authority = authority
1447 .lock()
1448 .unwrap_or_else(std::sync::PoisonError::into_inner);
1449 authority.state().clone()
1450 };
1451 Self::preview_dsl_input_on_state(&state, input, context)
1452 }
1453
1454 async fn session_dsl_state(
1455 &self,
1456 session_id: &SessionId,
1457 ) -> Result<dsl::MeerkatMachineState, RuntimeControlPlaneError> {
1458 let authority = self
1459 .session_dsl_authority(session_id)
1460 .await
1461 .map_err(RuntimeControlPlaneError::Internal)?;
1462 let authority = authority
1463 .lock()
1464 .unwrap_or_else(std::sync::PoisonError::into_inner);
1465 Ok(authority.state().clone())
1466 }
1467
1468 async fn commit_session_dsl_transition(
1469 &self,
1470 session_id: &SessionId,
1471 staged: StagedSessionDslInput,
1472 context: &str,
1473 ) -> Result<(), String> {
1474 self.commit_session_dsl_transition_with_dispatch_failure(
1475 session_id,
1476 staged,
1477 context,
1478 CommittedEffectDispatchFailure::PreserveCommittedDslState,
1479 )
1480 .await
1481 }
1482
1483 async fn commit_session_dsl_transition_preserving_committed_state(
1484 &self,
1485 session_id: &SessionId,
1486 staged: StagedSessionDslInput,
1487 context: &str,
1488 ) -> Result<(), String> {
1489 self.commit_session_dsl_transition_with_dispatch_failure(
1490 session_id,
1491 staged,
1492 context,
1493 CommittedEffectDispatchFailure::PreserveCommittedDslState,
1494 )
1495 .await
1496 }
1497
1498 async fn commit_session_dsl_transition_with_dispatch_failure(
1499 &self,
1500 _session_id: &SessionId,
1501 staged: StagedSessionDslInput,
1502 context: &str,
1503 dispatch_failure: CommittedEffectDispatchFailure,
1504 ) -> Result<(), String> {
1505 if let Err(error) = self
1506 .dispatch_routed_signals_from_effects(&staged.effects)
1507 .await
1508 {
1509 let CommittedEffectDispatchFailure::PreserveCommittedDslState = dispatch_failure;
1510 return Err(format!(
1511 "DSL authority ({context}): committed effect dispatch failed: {error}"
1512 ));
1513 }
1514 Ok(())
1515 }
1516
1517 async fn dispatch_routed_signals_from_effects(
1518 &self,
1519 effects: &[dsl::MeerkatMachineEffect],
1520 ) -> Result<(), String> {
1521 let dispatcher = {
1522 self.composition_signal_dispatcher
1523 .read()
1524 .unwrap_or_else(std::sync::PoisonError::into_inner)
1525 .clone()
1526 };
1527 let Some(dispatcher) = dispatcher else {
1528 return Ok(());
1529 };
1530
1531 for effect in effects {
1532 if let Some(signal) = composition::lift_routed_signal(effect) {
1533 composition::dispatch_routed_signal(&dispatcher, signal).await?;
1534 }
1535 }
1536 Ok(())
1537 }
1538
1539 async fn clear_dead_runtime_attachment(&self, session_id: &SessionId) {
1540 let mut sessions = self.sessions.write().await;
1541 if let Some(entry) = sessions.get_mut(session_id) {
1542 let cleared = entry.clear_dead_attachment();
1543 if cleared && let Err(error) = entry.stage_generated_executor_exit_observation() {
1544 tracing::warn!(
1545 %session_id,
1546 error = %error,
1547 "generated MeerkatMachine rejected executor-exit observation while clearing dead attachment"
1548 );
1549 }
1550 }
1551 }
1552
1553 async fn dispatch_cancel_after_boundary_runtime_effect(
1554 &self,
1555 session_id: &SessionId,
1556 effect_tx: Option<mpsc::Sender<crate::effect::RuntimeEffect>>,
1557 boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
1558 projected_effect: crate::effect::ProjectedRuntimeEffect,
1559 context: &str,
1560 ) -> Result<(), RuntimeDriverError> {
1561 let Some(effect_tx) = effect_tx else {
1562 let state = self
1563 .existing_session_runtime_state(session_id)
1564 .await
1565 .unwrap_or(RuntimeState::Destroyed);
1566 return Err(RuntimeDriverError::NotReady { state });
1567 };
1568
1569 let reason = projected_effect.reason().to_string();
1570 if let Some(boundary_handle) = boundary_handle {
1571 boundary_handle
1572 .cancel_after_boundary(reason)
1573 .await
1574 .map_err(|err| {
1575 RuntimeDriverError::Internal(format!(
1576 "{context}: failed to apply live boundary cancel: {err}"
1577 ))
1578 })?;
1579 }
1580
1581 match effect_tx.send(projected_effect.into_effect()).await {
1582 Ok(()) => Ok(()),
1583 Err(_) => {
1584 self.clear_dead_runtime_attachment(session_id).await;
1585 Err(RuntimeDriverError::NotReady {
1586 state: RuntimeState::Idle,
1587 })
1588 }
1589 }
1590 }
1591
1592 async fn restore_session_dsl_state(
1593 &self,
1594 session_id: &SessionId,
1595 snapshot: dsl::MeerkatMachineAuthoritySnapshot,
1596 ) {
1597 if let Ok(authority) = self.session_dsl_authority(session_id).await {
1598 Self::restore_dsl_authority_snapshot(&authority, snapshot);
1599 }
1600 }
1601
1602 async fn restore_session_dsl_state_if_current(
1603 &self,
1604 session_id: &SessionId,
1605 expected_current: dsl::MeerkatMachineAuthoritySnapshot,
1606 restore: dsl::MeerkatMachineAuthoritySnapshot,
1607 ) -> bool {
1608 let Ok(authority) = self.session_dsl_authority(session_id).await else {
1609 return false;
1610 };
1611 Self::restore_dsl_authority_snapshot_if_current(&authority, expected_current, restore)
1612 }
1613
1614 fn restore_dsl_authority_snapshot(
1615 authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1616 snapshot: dsl::MeerkatMachineAuthoritySnapshot,
1617 ) {
1618 let mut authority = authority
1619 .lock()
1620 .unwrap_or_else(std::sync::PoisonError::into_inner);
1621 authority.restore_snapshot(snapshot);
1622 }
1623
1624 fn restore_dsl_authority_snapshot_if_current(
1625 authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1626 expected_current: dsl::MeerkatMachineAuthoritySnapshot,
1627 restore: dsl::MeerkatMachineAuthoritySnapshot,
1628 ) -> bool {
1629 let mut authority = authority
1630 .lock()
1631 .unwrap_or_else(std::sync::PoisonError::into_inner);
1632 let current = authority.snapshot();
1633 if current.state() == expected_current.state() {
1634 authority.restore_snapshot(restore);
1635 true
1636 } else {
1637 false
1638 }
1639 }
1640}
1641
1642#[derive(Debug, Clone, Copy)]
1645pub struct MachineSessionControlAuthority {
1646 _private: (),
1647}
1648
1649#[cfg(feature = "live")]
1650struct LiveOpenAdmissionGeneratedAuthorityBridgeToken;
1651
1652#[cfg(feature = "live")]
1653struct LiveCloseResultGeneratedAuthorityBridgeToken;
1654
1655#[cfg(feature = "live")]
1656struct LiveChannelStatusResultGeneratedAuthorityBridgeToken;
1657
1658#[cfg(feature = "live")]
1659static LIVE_OPEN_ADMISSION_GENERATED_AUTHORITY_BRIDGE_TOKEN:
1660 LiveOpenAdmissionGeneratedAuthorityBridgeToken = LiveOpenAdmissionGeneratedAuthorityBridgeToken;
1661
1662#[cfg(feature = "live")]
1663static LIVE_CLOSE_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN:
1664 LiveCloseResultGeneratedAuthorityBridgeToken = LiveCloseResultGeneratedAuthorityBridgeToken;
1665
1666#[cfg(feature = "live")]
1667static LIVE_CHANNEL_STATUS_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN:
1668 LiveChannelStatusResultGeneratedAuthorityBridgeToken =
1669 LiveChannelStatusResultGeneratedAuthorityBridgeToken;
1670
1671#[cfg(feature = "live")]
1672fn live_open_admission_generated_authority_bridge_token()
1673-> &'static (dyn std::any::Any + Send + Sync) {
1674 &LIVE_OPEN_ADMISSION_GENERATED_AUTHORITY_BRIDGE_TOKEN
1675}
1676
1677#[cfg(feature = "live")]
1678fn live_close_result_generated_authority_bridge_token() -> &'static (dyn std::any::Any + Send + Sync)
1679{
1680 &LIVE_CLOSE_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN
1681}
1682
1683#[cfg(feature = "live")]
1684fn live_channel_status_result_generated_authority_bridge_token()
1685-> &'static (dyn std::any::Any + Send + Sync) {
1686 &LIVE_CHANNEL_STATUS_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN
1687}
1688
1689#[cfg(feature = "live")]
1690#[doc(hidden)]
1691#[allow(improper_ctypes_definitions, unsafe_code)]
1692#[unsafe(export_name = concat!(
1693 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_open_admission_",
1694 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1695))]
1696pub extern "Rust" fn live_open_admission_generated_authority_bridge_token_is_valid(
1697 token: &(dyn std::any::Any + Send + Sync),
1698) -> bool {
1699 token.is::<LiveOpenAdmissionGeneratedAuthorityBridgeToken>()
1700}
1701
1702#[cfg(feature = "live")]
1703#[doc(hidden)]
1704#[allow(improper_ctypes_definitions, unsafe_code)]
1705#[unsafe(export_name = concat!(
1706 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_close_result_",
1707 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1708))]
1709pub extern "Rust" fn live_close_result_generated_authority_bridge_token_is_valid(
1710 token: &(dyn std::any::Any + Send + Sync),
1711) -> bool {
1712 token.is::<LiveCloseResultGeneratedAuthorityBridgeToken>()
1713}
1714
1715#[cfg(feature = "live")]
1716#[doc(hidden)]
1717#[allow(improper_ctypes_definitions, unsafe_code)]
1718#[unsafe(export_name = concat!(
1719 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_channel_status_result_",
1720 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1721))]
1722pub extern "Rust" fn live_channel_status_result_generated_authority_bridge_token_is_valid(
1723 token: &(dyn std::any::Any + Send + Sync),
1724) -> bool {
1725 token.is::<LiveChannelStatusResultGeneratedAuthorityBridgeToken>()
1726}
1727
1728#[cfg(feature = "live")]
1729fn build_live_channel_open_authority(
1730 session_id: SessionId,
1731 channel_id: meerkat_live::LiveChannelId,
1732 sequence: u64,
1733) -> Result<meerkat_live::LiveChannelOpenAuthority, String> {
1734 #[allow(improper_ctypes_definitions, unsafe_code)]
1735 unsafe extern "Rust" {
1736 #[link_name = concat!(
1737 "__meerkat_live_runtime_generated_live_channel_open_authority_build_v1_",
1738 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1739 )]
1740 fn live_generated_channel_open_authority_build(
1741 token: &'static (dyn std::any::Any + Send + Sync),
1742 session_id: SessionId,
1743 channel_id: meerkat_live::LiveChannelId,
1744 sequence: u64,
1745 ) -> Result<meerkat_live::LiveChannelOpenAuthority, String>;
1746 }
1747 #[allow(unsafe_code)]
1748 unsafe {
1749 live_generated_channel_open_authority_build(
1750 live_open_admission_generated_authority_bridge_token(),
1751 session_id,
1752 channel_id,
1753 sequence,
1754 )
1755 }
1756}
1757
1758#[cfg(feature = "live")]
1759fn build_live_channel_close_commit_authority(
1760 channel_id: String,
1761 close_sequence: u64,
1762) -> Result<meerkat_live::LiveChannelCloseCommitAuthority, String> {
1763 #[allow(improper_ctypes_definitions, unsafe_code)]
1764 unsafe extern "Rust" {
1765 #[link_name = concat!(
1766 "__meerkat_live_runtime_generated_live_channel_close_commit_authority_build_v1_",
1767 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1768 )]
1769 fn live_generated_channel_close_commit_authority_build(
1770 token: &'static (dyn std::any::Any + Send + Sync),
1771 channel_id: String,
1772 close_sequence: u64,
1773 ) -> Result<meerkat_live::LiveChannelCloseCommitAuthority, String>;
1774 }
1775 #[allow(unsafe_code)]
1776 unsafe {
1777 live_generated_channel_close_commit_authority_build(
1778 live_close_result_generated_authority_bridge_token(),
1779 channel_id,
1780 close_sequence,
1781 )
1782 }
1783}
1784
1785#[cfg(feature = "live")]
1786fn build_live_channel_status_commit_authority(
1787 channel_id: String,
1788 status_observation_sequence: u64,
1789) -> Result<meerkat_live::LiveChannelStatusCommitAuthority, String> {
1790 #[allow(improper_ctypes_definitions, unsafe_code)]
1791 unsafe extern "Rust" {
1792 #[link_name = concat!(
1793 "__meerkat_live_runtime_generated_live_channel_status_commit_authority_build_v1_",
1794 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1795 )]
1796 fn live_generated_channel_status_commit_authority_build(
1797 token: &'static (dyn std::any::Any + Send + Sync),
1798 channel_id: String,
1799 status_observation_sequence: u64,
1800 ) -> Result<meerkat_live::LiveChannelStatusCommitAuthority, String>;
1801 }
1802 #[allow(unsafe_code)]
1803 unsafe {
1804 live_generated_channel_status_commit_authority_build(
1805 live_channel_status_result_generated_authority_bridge_token(),
1806 channel_id,
1807 status_observation_sequence,
1808 )
1809 }
1810}
1811
1812#[derive(Debug, Clone)]
1819#[cfg(feature = "live")]
1820pub struct LiveOpenAdmissionAuthority {
1821 session_id: SessionId,
1822 channel_id: meerkat_live::LiveChannelId,
1823 admitted: bool,
1824 rejection: Option<dsl::LiveOpenAdmissionRejection>,
1825 bound_llm_identity: Option<meerkat_core::SessionLlmIdentity>,
1826 sequence: u64,
1827 channel_open_authority: Option<meerkat_live::LiveChannelOpenAuthority>,
1828}
1829
1830#[cfg(feature = "live")]
1831impl LiveOpenAdmissionAuthority {
1832 pub(crate) fn from_generated_effect(
1833 session_id: SessionId,
1834 channel_id: meerkat_live::LiveChannelId,
1835 admitted: bool,
1836 rejection: Option<dsl::LiveOpenAdmissionRejection>,
1837 bound_llm_identity: Option<dsl::SessionLlmIdentity>,
1838 sequence: u64,
1839 ) -> Result<Self, String> {
1840 let bound_llm_identity = match (admitted, bound_llm_identity) {
1841 (true, Some(identity)) => Some(identity.try_into()?),
1842 (true, None) => {
1843 return Err(
1844 "generated live-open admission was admitted without bound LLM identity"
1845 .to_string(),
1846 );
1847 }
1848 (false, _) => None,
1849 };
1850 let channel_open_authority = if admitted {
1851 Some(build_live_channel_open_authority(
1852 session_id.clone(),
1853 channel_id.clone(),
1854 sequence,
1855 )?)
1856 } else {
1857 None
1858 };
1859 Ok(Self {
1860 session_id,
1861 channel_id,
1862 admitted,
1863 rejection,
1864 bound_llm_identity,
1865 sequence,
1866 channel_open_authority,
1867 })
1868 }
1869
1870 #[must_use]
1871 pub fn session_id(&self) -> &SessionId {
1872 &self.session_id
1873 }
1874
1875 #[must_use]
1876 pub fn channel_id(&self) -> &meerkat_live::LiveChannelId {
1877 &self.channel_id
1878 }
1879
1880 #[must_use]
1881 pub fn admitted(&self) -> bool {
1882 self.admitted
1883 }
1884
1885 #[must_use]
1886 pub fn rejection(&self) -> Option<dsl::LiveOpenAdmissionRejection> {
1887 self.rejection
1888 }
1889
1890 #[must_use]
1891 pub fn bound_llm_identity(&self) -> Option<&meerkat_core::SessionLlmIdentity> {
1892 self.bound_llm_identity.as_ref()
1893 }
1894
1895 #[must_use]
1896 pub fn sequence(&self) -> u64 {
1897 self.sequence
1898 }
1899
1900 #[must_use]
1901 pub fn channel_open_authority(&self) -> Option<&meerkat_live::LiveChannelOpenAuthority> {
1902 self.channel_open_authority.as_ref()
1903 }
1904}
1905
1906#[derive(Debug, Clone, PartialEq, Eq)]
1913#[cfg(feature = "live")]
1914pub struct LiveRefreshResultAuthority {
1915 pub status: dsl::LiveRefreshPublicStatus,
1916 pub sequence: u64,
1917 pub queue_acceptance_sequence: u64,
1918}
1919
1920#[derive(Debug, Clone)]
1925#[cfg(feature = "live")]
1926pub struct LiveCloseResultAuthority {
1927 pub status: dsl::LiveClosePublicStatus,
1928 pub sequence: u64,
1929 pub close_observation_sequence: u64,
1930 channel_close_commit_authority: Option<meerkat_live::LiveChannelCloseCommitAuthority>,
1931}
1932
1933#[cfg(feature = "live")]
1934impl LiveCloseResultAuthority {
1935 pub(crate) fn from_generated_effect(
1936 channel_id: String,
1937 status: dsl::LiveClosePublicStatus,
1938 sequence: u64,
1939 close_observation_sequence: u64,
1940 ) -> Result<Self, String> {
1941 let channel_close_commit_authority = match status {
1942 dsl::LiveClosePublicStatus::Closed => Some(build_live_channel_close_commit_authority(
1943 channel_id,
1944 close_observation_sequence,
1945 )?),
1946 };
1947 Ok(Self {
1948 status,
1949 sequence,
1950 close_observation_sequence,
1951 channel_close_commit_authority,
1952 })
1953 }
1954
1955 #[must_use]
1956 pub fn channel_close_commit_authority(
1957 &self,
1958 ) -> Option<&meerkat_live::LiveChannelCloseCommitAuthority> {
1959 self.channel_close_commit_authority.as_ref()
1960 }
1961
1962 #[must_use]
1963 pub fn into_channel_close_commit_authority(
1964 self,
1965 ) -> Option<meerkat_live::LiveChannelCloseCommitAuthority> {
1966 self.channel_close_commit_authority
1967 }
1968}
1969
1970#[derive(Debug, Clone, PartialEq, Eq)]
1976#[cfg(feature = "live")]
1977pub struct LiveCommandResultAuthority {
1978 pub command: dsl::LiveCommandPublicKind,
1979 pub sequence: u64,
1980 pub command_acceptance_sequence: u64,
1981}
1982
1983#[derive(Debug, Clone, PartialEq, Eq)]
1990#[cfg(feature = "live")]
1991pub struct LiveCommandRejectionAuthority {
1992 pub command: dsl::LiveCommandPublicKind,
1993 pub rejection: dsl::LiveCommandRejectionReason,
1994 pub public_error_class: dsl::LiveCommandRejectionPublicErrorClass,
1995 pub sequence: u64,
1996}
1997
1998#[derive(Debug, Clone, PartialEq, Eq)]
2005#[cfg(feature = "live")]
2006pub struct LiveChannelRequestRejectionAuthority {
2007 pub request: dsl::LiveChannelRequestPublicKind,
2008 pub rejection: dsl::LiveChannelRequestRejectionReason,
2009 pub public_error_class: dsl::LiveChannelRequestRejectionPublicErrorClass,
2010 pub sequence: u64,
2011}
2012
2013#[derive(Debug, Clone, PartialEq, Eq)]
2020#[cfg(feature = "live")]
2021pub struct LiveWebrtcTokenAuthority {
2022 pub token: String,
2023 pub expires_at_ms: u64,
2024 pub sequence: u64,
2025}
2026
2027#[derive(Debug, Clone, PartialEq, Eq)]
2033#[cfg(feature = "live")]
2034pub struct LiveWebrtcAnswerAdmissionAuthority {
2035 pub admitted: bool,
2036 pub rejection: Option<dsl::LiveWebrtcAnswerAdmissionRejection>,
2037 pub public_error_class: Option<dsl::LiveChannelRequestRejectionPublicErrorClass>,
2038 pub sequence: u64,
2039}
2040
2041#[derive(Debug, Clone, PartialEq, Eq)]
2048#[cfg(feature = "live")]
2049pub struct LiveWebrtcAnswerResultAuthority {
2050 pub status: dsl::LiveWebrtcAnswerPublicStatus,
2051 pub answered: bool,
2052 pub sequence: u64,
2053 pub answer_observation_sequence: u64,
2054}
2055
2056#[derive(Debug, Clone, PartialEq, Eq)]
2063#[cfg(feature = "live")]
2064pub struct LiveWebsocketTokenAuthority {
2065 pub token: String,
2066 pub expires_at_ms: u64,
2067 pub sequence: u64,
2068}
2069
2070#[derive(Debug, Clone, PartialEq, Eq)]
2076#[cfg(feature = "live")]
2077pub struct LiveWebsocketTokenAdmissionAuthority {
2078 pub admitted: bool,
2079 pub rejection: Option<dsl::LiveWebsocketTokenAdmissionRejection>,
2080 pub public_error_class: Option<dsl::LiveWebsocketTokenAdmissionPublicErrorClass>,
2081 pub sequence: u64,
2082}
2083
2084#[derive(Debug, Clone)]
2090#[cfg(feature = "live")]
2091pub struct LiveChannelStatusAuthority {
2092 pub status: dsl::LiveChannelPublicStatus,
2093 pub sequence: u64,
2094 pub status_observation_sequence: u64,
2095 pub degradation_reason: Option<dsl::LiveChannelDegradationReason>,
2096 pub degradation_detail: Option<String>,
2097 pub channel_status_commit_authority: Option<meerkat_live::LiveChannelStatusCommitAuthority>,
2098}
2099
2100#[cfg(feature = "live")]
2101impl LiveChannelStatusAuthority {
2102 pub(crate) fn from_generated_effect(
2103 channel_id: String,
2104 status: dsl::LiveChannelPublicStatus,
2105 sequence: u64,
2106 status_observation_sequence: u64,
2107 degradation_reason: Option<dsl::LiveChannelDegradationReason>,
2108 degradation_detail: Option<String>,
2109 ) -> Result<Self, String> {
2110 Ok(Self {
2111 status,
2112 sequence,
2113 status_observation_sequence,
2114 degradation_reason,
2115 degradation_detail,
2116 channel_status_commit_authority: Some(build_live_channel_status_commit_authority(
2117 channel_id,
2118 status_observation_sequence,
2119 )?),
2120 })
2121 }
2122
2123 #[must_use]
2124 pub fn channel_status_commit_authority(
2125 &self,
2126 ) -> Option<&meerkat_live::LiveChannelStatusCommitAuthority> {
2127 self.channel_status_commit_authority.as_ref()
2128 }
2129
2130 #[must_use]
2131 pub fn into_channel_status_commit_authority(
2132 self,
2133 ) -> Option<meerkat_live::LiveChannelStatusCommitAuthority> {
2134 self.channel_status_commit_authority
2135 }
2136}
2137
2138pub struct MeerkatMachine {
2145 sessions: RwLock<HashMap<SessionId, RuntimeSessionEntry>>,
2147 store: Option<Arc<dyn RuntimeStore>>,
2149 blob_store: Option<Arc<dyn BlobStore>>,
2151 llm_reconfigure_host: StdRwLock<Option<Arc<dyn SessionLlmReconfigureHost>>>,
2153 auth_lease: StdRwLock<meerkat_core::handles::GeneratedAuthLeaseHandle>,
2156 #[cfg(not(target_arch = "wasm32"))]
2159 oauth_flows: StdRwLock<Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority>>,
2160 #[cfg(feature = "live")]
2164 live_unbound_rejection_authority: crate::driver::ephemeral::SharedIngressDslAuthority,
2165 session_claims: Arc<crate::handles::RuntimeSessionClaimRegistry>,
2172 composition_signal_dispatcher:
2176 StdRwLock<Option<composition::MeerkatCompositionSignalDispatcher>>,
2177}
2178
2179impl MeerkatMachine {
2180 #[must_use]
2183 pub fn session_control_authority(&self) -> MachineSessionControlAuthority {
2184 MachineSessionControlAuthority { _private: () }
2185 }
2186
2187 #[must_use]
2192 pub fn shares_runtime_persistence_with(&self, other: &Self) -> bool {
2193 match (&self.store, &other.store) {
2194 (None, None) => true,
2195 (Some(a), Some(b)) => runtime_stores_share_authority(a, b),
2196 _ => false,
2197 }
2198 }
2199
2200 #[must_use]
2203 pub fn shares_runtime_store_authority(&self, store: &Arc<dyn RuntimeStore>) -> bool {
2204 self.store
2205 .as_ref()
2206 .is_some_and(|machine_store| runtime_stores_share_authority(machine_store, store))
2207 }
2208
2209 #[must_use]
2211 pub fn has_runtime_persistence(&self) -> bool {
2212 self.store.is_some()
2213 }
2214
2215 fn normalize_destroyed_error(err: RuntimeDriverError) -> RuntimeDriverError {
2216 match err {
2217 RuntimeDriverError::NotReady {
2218 state: RuntimeState::Destroyed,
2219 } => RuntimeDriverError::Destroyed,
2220 other => other,
2221 }
2222 }
2223
2224 pub fn ephemeral() -> Self {
2226 let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
2227 #[cfg(not(target_arch = "wasm32"))]
2228 let oauth_flows = Arc::new(crate::handles::RuntimeOAuthFlowHandle::new_with_auth_lease(
2229 std::time::Duration::from_secs(10 * 60),
2230 Arc::clone(&auth_lease),
2231 ));
2232 let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
2233 Self {
2234 sessions: RwLock::new(HashMap::new()),
2235 store: None,
2236 blob_store: None,
2237 llm_reconfigure_host: StdRwLock::new(None),
2238 auth_lease: StdRwLock::new(auth_lease),
2239 #[cfg(not(target_arch = "wasm32"))]
2240 oauth_flows: StdRwLock::new(oauth_flows),
2241 #[cfg(feature = "live")]
2242 live_unbound_rejection_authority: live_unbound_rejection_authority(),
2243 session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
2244 composition_signal_dispatcher: StdRwLock::new(None),
2245 }
2246 }
2247
2248 pub fn persistent(store: Arc<dyn RuntimeStore>, blob_store: Arc<dyn BlobStore>) -> Self {
2250 #[cfg(not(target_arch = "wasm32"))]
2251 let (auth_lease, oauth_flows) = {
2252 let authorities = persistent_auth_authorities(&store);
2253 (
2254 Arc::clone(&authorities.auth_lease),
2255 Arc::clone(&authorities.oauth_flows),
2256 )
2257 };
2258 #[cfg(target_arch = "wasm32")]
2259 let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
2260 let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
2261 Self {
2262 sessions: RwLock::new(HashMap::new()),
2263 store: Some(store),
2264 blob_store: Some(blob_store),
2265 llm_reconfigure_host: StdRwLock::new(None),
2266 auth_lease: StdRwLock::new(auth_lease),
2267 #[cfg(not(target_arch = "wasm32"))]
2268 oauth_flows: StdRwLock::new(oauth_flows),
2269 #[cfg(feature = "live")]
2270 live_unbound_rejection_authority: live_unbound_rejection_authority(),
2271 session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
2272 composition_signal_dispatcher: StdRwLock::new(None),
2273 }
2274 }
2275
2276 pub fn persistent_without_blobs(store: Arc<dyn RuntimeStore>) -> Self {
2282 #[cfg(not(target_arch = "wasm32"))]
2283 let (auth_lease, oauth_flows) = {
2284 let authorities = persistent_auth_authorities(&store);
2285 (
2286 Arc::clone(&authorities.auth_lease),
2287 Arc::clone(&authorities.oauth_flows),
2288 )
2289 };
2290 #[cfg(target_arch = "wasm32")]
2291 let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
2292 let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
2293 Self {
2294 sessions: RwLock::new(HashMap::new()),
2295 store: Some(store),
2296 blob_store: Some(Arc::new(UnavailableBlobStore)),
2297 llm_reconfigure_host: StdRwLock::new(None),
2298 auth_lease: StdRwLock::new(auth_lease),
2299 #[cfg(not(target_arch = "wasm32"))]
2300 oauth_flows: StdRwLock::new(oauth_flows),
2301 #[cfg(feature = "live")]
2302 live_unbound_rejection_authority: live_unbound_rejection_authority(),
2303 session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
2304 composition_signal_dispatcher: StdRwLock::new(None),
2305 }
2306 }
2307
2308 pub fn auth_lease_handle(&self) -> Arc<dyn meerkat_core::handles::AuthLeaseHandle> {
2311 self.generated_auth_lease_handle().clone_handle()
2312 }
2313
2314 pub fn generated_auth_lease_handle(&self) -> meerkat_core::handles::GeneratedAuthLeaseHandle {
2317 self.auth_lease
2318 .read()
2319 .unwrap_or_else(std::sync::PoisonError::into_inner)
2320 .clone()
2321 }
2322
2323 pub fn set_auth_lease_handle(&self, handle: Arc<crate::handles::RuntimeAuthLeaseHandle>) {
2329 self.set_runtime_auth_lease_handle(handle);
2330 }
2331
2332 #[cfg(not(target_arch = "wasm32"))]
2338 pub fn set_auth_lease_handle_with_oauth_flow_authority(
2339 &self,
2340 handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
2341 oauth_flows: Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority>,
2342 ) {
2343 *self
2344 .oauth_flows
2345 .write()
2346 .unwrap_or_else(std::sync::PoisonError::into_inner) = oauth_flows;
2347 let handle = generated_runtime_auth_lease_handle(handle);
2348 *self
2349 .auth_lease
2350 .write()
2351 .unwrap_or_else(std::sync::PoisonError::into_inner) = handle;
2352 }
2353
2354 pub fn set_runtime_auth_lease_handle(
2357 &self,
2358 handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
2359 ) {
2360 #[cfg(not(target_arch = "wasm32"))]
2361 {
2362 *self
2363 .oauth_flows
2364 .write()
2365 .unwrap_or_else(std::sync::PoisonError::into_inner) =
2366 Arc::new(crate::handles::RuntimeOAuthFlowHandle::new_with_auth_lease(
2367 std::time::Duration::from_secs(10 * 60),
2368 Arc::clone(&handle),
2369 ));
2370 }
2371 let handle = generated_runtime_auth_lease_handle(handle);
2372 *self
2373 .auth_lease
2374 .write()
2375 .unwrap_or_else(std::sync::PoisonError::into_inner) = handle;
2376 }
2377
2378 #[cfg(not(target_arch = "wasm32"))]
2381 pub fn oauth_flow_authority(
2382 &self,
2383 ) -> Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority> {
2384 Arc::clone(
2385 &self
2386 .oauth_flows
2387 .read()
2388 .unwrap_or_else(std::sync::PoisonError::into_inner),
2389 )
2390 }
2391
2392 pub fn session_claim_handle(&self) -> Arc<dyn meerkat_core::handles::SessionClaimHandle> {
2397 Arc::clone(&self.session_claims) as Arc<dyn meerkat_core::handles::SessionClaimHandle>
2398 }
2399
2400 pub fn set_composition_signal_dispatcher(
2403 &self,
2404 dispatcher: composition::MeerkatCompositionSignalDispatcher,
2405 ) {
2406 let mut slot = self
2407 .composition_signal_dispatcher
2408 .write()
2409 .unwrap_or_else(std::sync::PoisonError::into_inner);
2410 *slot = Some(dispatcher);
2411 }
2412
2413 pub(crate) async fn apply_routed_meerkat_input(
2424 &self,
2425 session_id: &SessionId,
2426 input: dsl::MeerkatMachineInput,
2427 ) -> Result<(), dsl_authority::DslTransitionRefusal> {
2428 let _gate_guard = self
2429 .lock_current_session_mutation_gate(session_id)
2430 .await
2431 .ok_or_else(|| {
2432 dsl_authority::DslTransitionRefusal::other(
2433 "routed_session_not_registered",
2434 format!(
2435 "session `{session_id}` is not registered with this MeerkatMachine; \
2436 cannot deliver routed input"
2437 ),
2438 )
2439 })?;
2440 self.apply_routed_session_dsl_input(session_id, input, "RoutedMeerkatInput")
2441 .await
2442 .map(|_| ())
2443 }
2444
2445 #[cfg(test)]
2446 pub(crate) async fn debug_shared_ingress_authorities(
2447 &self,
2448 session_id: &SessionId,
2449 ) -> Option<(
2450 Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
2451 crate::driver::ephemeral::SharedIngressDslAuthority,
2452 )> {
2453 let sessions = self.sessions.read().await;
2454 let entry = sessions.get(session_id)?;
2455 let session_authority = Arc::clone(&entry.dsl_authority);
2456 let driver = entry.driver.lock().await;
2457 Some((session_authority, driver.shared_dsl_authority()))
2458 }
2459
2460 fn make_driver(
2462 &self,
2463 runtime_id: LogicalRuntimeId,
2464 dsl_authority: crate::driver::ephemeral::SharedIngressDslAuthority,
2465 initial_runtime_state: RuntimeState,
2466 ) -> DriverEntry {
2467 let control_projection = Arc::new(StdRwLock::new(
2468 crate::driver::ephemeral::RuntimeControlProjection {
2469 phase: initial_runtime_state,
2470 current_run_id: None,
2471 pre_run_phase: None,
2472 },
2473 ));
2474 match (&self.store, &self.blob_store) {
2475 (Some(store), Some(blob_store)) => {
2476 DriverEntry::Persistent(PersistentRuntimeDriver::new_with_control(
2477 runtime_id,
2478 store.clone(),
2479 blob_store.clone(),
2480 control_projection,
2481 dsl_authority,
2482 ))
2483 }
2484 _ => DriverEntry::Ephemeral(EphemeralRuntimeDriver::new_with_control_and_dsl(
2485 runtime_id,
2486 control_projection,
2487 dsl_authority,
2488 )),
2489 }
2490 }
2491
2492 async fn recover_or_create_ops_state(
2499 &self,
2500 session_id: &SessionId,
2501 runtime_id: &LogicalRuntimeId,
2502 ) -> Result<
2503 (
2504 Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
2505 meerkat_core::RuntimeEpochId,
2506 Arc<meerkat_core::EpochCursorState>,
2507 ),
2508 RuntimeDriverError,
2509 > {
2510 if let Some(ref store) = self.store {
2511 match store.load_ops_lifecycle(runtime_id).await {
2512 Ok(Some(snapshot)) => {
2513 let recovered_epoch = snapshot.epoch_id.clone();
2514 let recovered_ops_count = snapshot.completion_entries.len();
2515 let registry =
2516 match crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::from_recovered(
2517 snapshot,
2518 ) {
2519 Ok(registry) => registry,
2520 Err(err) => {
2521 tracing::error!(
2522 %session_id,
2523 %runtime_id,
2524 error = %err,
2525 "failed to recover ops lifecycle through generated authority"
2526 );
2527 return Err(RuntimeDriverError::Internal(format!(
2528 "failed to recover ops lifecycle through generated authority: {err}"
2529 )));
2530 }
2531 };
2532 let recovered_cursor_snapshot = registry.completion_cursor_snapshot();
2533 let recovered_cursors = meerkat_core::EpochCursorState::from_recovered(
2534 recovered_cursor_snapshot.agent_applied_cursor,
2535 recovered_cursor_snapshot.runtime_observed_seq,
2536 recovered_cursor_snapshot.runtime_last_injected_seq,
2537 );
2538 tracing::info!(
2539 %session_id,
2540 %runtime_id,
2541 epoch_id = %recovered_epoch,
2542 recovered_ops = recovered_ops_count,
2543 "ops lifecycle recovered from durable store (same epoch)"
2544 );
2545 return Ok((
2546 Arc::new(registry),
2547 recovered_epoch,
2548 Arc::new(recovered_cursors),
2549 ));
2550 }
2551 Ok(None) => {}
2552 Err(err) => {
2553 tracing::error!(
2554 %session_id,
2555 %runtime_id,
2556 error = %err,
2557 "failed to load ops lifecycle from durable store"
2558 );
2559 return Err(RuntimeDriverError::Internal(format!(
2560 "failed to load ops lifecycle from durable store: {err}"
2561 )));
2562 }
2563 }
2564 tracing::debug!(%session_id, "no persisted ops lifecycle; fresh epoch");
2565 Ok((
2566 Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new()),
2567 meerkat_core::RuntimeEpochId::new(),
2568 Arc::new(meerkat_core::EpochCursorState::new()),
2569 ))
2570 } else {
2571 Ok((
2572 Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new()),
2573 meerkat_core::RuntimeEpochId::new(),
2574 Arc::new(meerkat_core::EpochCursorState::new()),
2575 ))
2576 }
2577 }
2578
2579 fn fresh_ops_state() -> (
2580 Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
2581 meerkat_core::RuntimeEpochId,
2582 Arc<meerkat_core::EpochCursorState>,
2583 ) {
2584 let registry = Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new());
2585 let epoch = meerkat_core::RuntimeEpochId::new();
2586 let cursors = Arc::new(meerkat_core::EpochCursorState::new());
2587 (registry, epoch, cursors)
2588 }
2589
2590 #[allow(clippy::large_futures)]
2591 fn execute_meerkat_machine_command(
2592 &self,
2593 self_handle: Option<Arc<Self>>,
2594 command: MeerkatMachineCommand,
2595 ) -> MeerkatMachineCommandFuture<'_> {
2596 Box::pin(async move {
2597 match command {
2598 MeerkatMachineCommand::EnsureSessionWithExecutor { .. } => {
2599 let self_handle = self_handle.ok_or_else(|| {
2600 MeerkatMachineCommandError::Driver(RuntimeDriverError::Internal(
2601 "EnsureSessionWithExecutor requires Arc<Self> machine handle".into(),
2602 ))
2603 })?;
2604 self_handle
2605 .execute_meerkat_machine_ensure_session_command(command)
2606 .await
2607 .map_err(Into::into)
2608 }
2609 MeerkatMachineCommand::RegisterSession { .. }
2610 | MeerkatMachineCommand::UnregisterSession { .. }
2611 | MeerkatMachineCommand::SetSilentIntents { .. }
2612 | MeerkatMachineCommand::CancelAfterBoundary { .. }
2613 | MeerkatMachineCommand::StopRuntimeExecutor { .. }
2614 | MeerkatMachineCommand::CommitServiceTurnTerminalReceipt { .. }
2615 | MeerkatMachineCommand::ContainsSession { .. }
2616 | MeerkatMachineCommand::SessionHasExecutor { .. }
2617 | MeerkatMachineCommand::SessionHasComms { .. }
2618 | MeerkatMachineCommand::OpsLifecycleRegistry { .. }
2619 | MeerkatMachineCommand::PrepareBindings { .. }
2620 | MeerkatMachineCommand::PrepareLocalSessionBindings { .. }
2621 | MeerkatMachineCommand::InputState { .. }
2622 | MeerkatMachineCommand::InputStateByIdempotencyKey { .. }
2623 | MeerkatMachineCommand::InteractionTerminalStatus { .. }
2624 | MeerkatMachineCommand::RunTerminalStatus { .. }
2625 | MeerkatMachineCommand::ListActiveInputs { .. }
2626 | MeerkatMachineCommand::ReconfigureSessionLlmIdentity { .. }
2627 | MeerkatMachineCommand::StagePersistentFilter { .. }
2628 | MeerkatMachineCommand::RequestDeferredTools { .. }
2629 | MeerkatMachineCommand::PublishCommittedVisibleSet { .. } => self
2630 .execute_meerkat_machine_session_command(command)
2631 .await
2632 .map_err(Into::into),
2633 MeerkatMachineCommand::SetPeerIngressContext { .. }
2634 | MeerkatMachineCommand::NotifyDrainExited { .. } => {
2635 let self_handle = self_handle.ok_or_else(|| {
2636 MeerkatMachineCommandError::Driver(RuntimeDriverError::Internal(
2637 "drain command requires Arc<Self> machine handle".into(),
2638 ))
2639 })?;
2640 self_handle
2641 .execute_meerkat_machine_drain_command(command)
2642 .await
2643 .map_err(Into::into)
2644 }
2645 MeerkatMachineCommand::AbortAll
2646 | MeerkatMachineCommand::Abort { .. }
2647 | MeerkatMachineCommand::Wait { .. } => self
2648 .execute_meerkat_machine_drain_local_command(command)
2649 .await
2650 .map_err(Into::into),
2651 MeerkatMachineCommand::Ingest { .. }
2652 | MeerkatMachineCommand::PublishEvent { .. }
2653 | MeerkatMachineCommand::Retire { .. }
2654 | MeerkatMachineCommand::Recycle { .. }
2655 | MeerkatMachineCommand::Reset { .. }
2656 | MeerkatMachineCommand::Recover { .. }
2657 | MeerkatMachineCommand::Destroy { .. }
2658 | MeerkatMachineCommand::RuntimeState { .. }
2659 | MeerkatMachineCommand::ResolvedSessionLlmCapabilities { .. }
2660 | MeerkatMachineCommand::ConfigureModelRoutingBaseline { .. }
2661 | MeerkatMachineCommand::SessionModelRoutingStatus { .. }
2662 | MeerkatMachineCommand::RequestSwitchTurn { .. }
2663 | MeerkatMachineCommand::AdmitModelRoutingAssistantTurn { .. }
2664 | MeerkatMachineCommand::BeginImageOperation { .. }
2665 | MeerkatMachineCommand::DenyImageOperationPlan { .. }
2666 | MeerkatMachineCommand::ActivateImageOperationOverride { .. }
2667 | MeerkatMachineCommand::ClassifyImageOperationTerminal { .. }
2668 | MeerkatMachineCommand::CompleteImageOperation { .. }
2669 | MeerkatMachineCommand::RestoreImageOperationOverride { .. }
2670 | MeerkatMachineCommand::LoadBoundaryReceipt { .. } => self
2671 .execute_meerkat_machine_control_command(command)
2672 .await
2673 .map_err(Into::into),
2674 MeerkatMachineCommand::AcceptWithCompletion { .. }
2675 | MeerkatMachineCommand::AcceptWithoutWake { .. } => self
2676 .execute_meerkat_machine_ingress_command(command)
2677 .await
2678 .map_err(Into::into),
2679 }
2680 })
2681 }
2682
2683 pub async fn register_session(
2690 &self,
2691 session_id: SessionId,
2692 ) -> Result<(), RuntimeControlPlaneError> {
2693 match self
2694 .execute_meerkat_machine_command(
2695 None,
2696 MeerkatMachineCommand::RegisterSession { session_id },
2697 )
2698 .await
2699 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
2700 {
2701 MeerkatMachineCommandResult::Unit => Ok(()),
2702 other => Err(RuntimeControlPlaneError::Internal(format!(
2703 "register_session: unexpected command result variant: {other:?}"
2704 ))),
2705 }
2706 }
2707}
2708
2709#[cfg(test)]
2710#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2711#[path = "../meerkat_machine_tests.rs"]
2712mod tests;