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