1use super::*;
2use crate::input_state::StoredInputState;
3
4#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
5#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
6impl SessionServiceRuntimeExt for MeerkatMachine {
7 async fn accept_input(
8 &self,
9 session_id: &SessionId,
10 input: Input,
11 ) -> Result<AcceptOutcome, RuntimeDriverError> {
12 match self
13 .execute_meerkat_machine_command(
14 None,
15 MeerkatMachineCommand::AcceptWithCompletion {
16 session_id: session_id.clone(),
17 input,
18 register_completion: false,
19 },
20 )
21 .await
22 .map_err(MeerkatMachine::driver_error_from_command_error)?
23 {
24 MeerkatMachineCommandResult::AcceptWithCompletion {
25 outcome,
26 handle: _,
27 admission_signal: _,
28 } => Ok(outcome),
29 other => Err(RuntimeDriverError::Internal(format!(
30 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::accept_input: {other:?}"
31 ))),
32 }
33 }
34
35 async fn accept_input_with_completion(
36 &self,
37 session_id: &SessionId,
38 input: Input,
39 ) -> Result<(AcceptOutcome, Option<crate::completion::CompletionHandle>), RuntimeDriverError>
40 {
41 tracing::debug!(
42 session_id = %session_id,
43 input_id = %input.id(),
44 "SessionServiceRuntimeExt::accept_input_with_completion entered"
45 );
46 self.accept_input_with_completion_boxed(session_id, input)
47 .await
48 }
49
50 async fn runtime_state(
51 &self,
52 session_id: &SessionId,
53 ) -> Result<RuntimeState, RuntimeDriverError> {
54 let runtime_id = MeerkatMachine::logical_runtime_id(session_id);
55 match self
56 .execute_meerkat_machine_command(
57 None,
58 MeerkatMachineCommand::RuntimeState { runtime_id },
59 )
60 .await
61 .map_err(MeerkatMachine::driver_error_from_command_error)?
62 {
63 MeerkatMachineCommandResult::RuntimeState(state) => Ok(state),
64 other => Err(RuntimeDriverError::Internal(format!(
65 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::runtime_state: {other:?}"
66 ))),
67 }
68 }
69
70 async fn retire_runtime(
71 &self,
72 session_id: &SessionId,
73 ) -> Result<RetireReport, RuntimeDriverError> {
74 let runtime_id = MeerkatMachine::logical_runtime_id(session_id);
75 match self
76 .execute_meerkat_machine_command(None, MeerkatMachineCommand::Retire { runtime_id })
77 .await
78 .map_err(MeerkatMachine::driver_error_from_command_error)?
79 {
80 MeerkatMachineCommandResult::RetireReport(report) => Ok(report),
81 other => Err(RuntimeDriverError::Internal(format!(
82 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::retire_runtime: {other:?}"
83 ))),
84 }
85 }
86
87 async fn reset_runtime(
88 &self,
89 session_id: &SessionId,
90 ) -> Result<ResetReport, RuntimeDriverError> {
91 let runtime_id = MeerkatMachine::logical_runtime_id(session_id);
92 match self
93 .execute_meerkat_machine_command(None, MeerkatMachineCommand::Reset { runtime_id })
94 .await
95 .map_err(MeerkatMachine::driver_error_from_command_error)?
96 {
97 MeerkatMachineCommandResult::ResetReport(report) => Ok(report),
98 other => Err(RuntimeDriverError::Internal(format!(
99 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::reset_runtime: {other:?}"
100 ))),
101 }
102 }
103
104 async fn input_state(
105 &self,
106 session_id: &SessionId,
107 input_id: &InputId,
108 ) -> Result<Option<StoredInputState>, RuntimeDriverError> {
109 match self
110 .execute_meerkat_machine_command(
111 None,
112 MeerkatMachineCommand::InputState {
113 session_id: session_id.clone(),
114 input_id: input_id.clone(),
115 },
116 )
117 .await
118 .map_err(MeerkatMachine::driver_error_from_command_error)?
119 {
120 MeerkatMachineCommandResult::InputState(state) => Ok(state),
121 other => Err(RuntimeDriverError::Internal(format!(
122 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::input_state: {other:?}"
123 ))),
124 }
125 }
126
127 async fn input_state_by_idempotency_key(
128 &self,
129 session_id: &SessionId,
130 idempotency_key: &str,
131 ) -> Result<Option<StoredInputState>, RuntimeDriverError> {
132 match self
133 .execute_meerkat_machine_command(
134 None,
135 MeerkatMachineCommand::InputStateByIdempotencyKey {
136 session_id: session_id.clone(),
137 idempotency_key: idempotency_key.to_string(),
138 },
139 )
140 .await
141 .map_err(MeerkatMachine::driver_error_from_command_error)?
142 {
143 MeerkatMachineCommandResult::InputState(state) => Ok(state),
144 other => Err(RuntimeDriverError::Internal(format!(
145 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::input_state_by_idempotency_key: {other:?}"
146 ))),
147 }
148 }
149
150 async fn interaction_terminal_status(
151 &self,
152 session_id: &SessionId,
153 selector: crate::terminal_status::InteractionSelector,
154 ) -> Result<
155 Option<crate::terminal_status::Sourced<crate::terminal_status::InteractionTerminalReport>>,
156 RuntimeDriverError,
157 > {
158 match self
159 .execute_meerkat_machine_command(
160 None,
161 MeerkatMachineCommand::InteractionTerminalStatus {
162 session_id: session_id.clone(),
163 selector,
164 },
165 )
166 .await
167 .map_err(MeerkatMachine::driver_error_from_command_error)?
168 {
169 MeerkatMachineCommandResult::InteractionTerminalStatus(report) => Ok(report),
170 other => Err(RuntimeDriverError::Internal(format!(
171 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::interaction_terminal_status: {other:?}"
172 ))),
173 }
174 }
175
176 async fn run_terminal_status(
177 &self,
178 session_id: &SessionId,
179 run_id: &meerkat_core::lifecycle::RunId,
180 ) -> Result<
181 crate::terminal_status::Sourced<crate::terminal_status::RunTerminalReport>,
182 RuntimeDriverError,
183 > {
184 match self
185 .execute_meerkat_machine_command(
186 None,
187 MeerkatMachineCommand::RunTerminalStatus {
188 session_id: session_id.clone(),
189 run_id: run_id.clone(),
190 },
191 )
192 .await
193 .map_err(MeerkatMachine::driver_error_from_command_error)?
194 {
195 MeerkatMachineCommandResult::RunTerminalStatus(report) => Ok(report),
196 other => Err(RuntimeDriverError::Internal(format!(
197 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::run_terminal_status: {other:?}"
198 ))),
199 }
200 }
201
202 async fn list_active_inputs(
203 &self,
204 session_id: &SessionId,
205 ) -> Result<Vec<InputId>, RuntimeDriverError> {
206 match self
207 .execute_meerkat_machine_command(
208 None,
209 MeerkatMachineCommand::ListActiveInputs {
210 session_id: session_id.clone(),
211 },
212 )
213 .await
214 .map_err(MeerkatMachine::driver_error_from_command_error)?
215 {
216 MeerkatMachineCommandResult::ActiveInputs(inputs) => Ok(inputs),
217 other => Err(RuntimeDriverError::Internal(format!(
218 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::list_active_inputs: {other:?}"
219 ))),
220 }
221 }
222
223 async fn reconfigure_session_llm_identity(
224 &self,
225 session_id: &SessionId,
226 request: SessionLlmReconfigureRequest,
227 ) -> Result<SessionLlmReconfigureReport, RuntimeDriverError> {
228 let command = self
229 .prepare_reconfigure_session_llm_command(session_id, request)
230 .await?;
231 match self
232 .execute_meerkat_machine_command(None, command)
233 .await
234 .map_err(MeerkatMachine::driver_error_from_command_error)?
235 {
236 MeerkatMachineCommandResult::LlmReconfigured(report) => Ok(report),
237 other => Err(RuntimeDriverError::Internal(format!(
238 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::reconfigure_session_llm_identity: {other:?}"
239 ))),
240 }
241 }
242
243 async fn resolved_session_llm_capabilities(
244 &self,
245 session_id: &SessionId,
246 ) -> Result<Option<SessionLlmCapabilitySurface>, RuntimeDriverError> {
247 match self
248 .execute_meerkat_machine_command(
249 None,
250 MeerkatMachineCommand::ResolvedSessionLlmCapabilities {
251 session_id: session_id.clone(),
252 },
253 )
254 .await
255 .map_err(MeerkatMachine::driver_error_from_command_error)?
256 {
257 MeerkatMachineCommandResult::ResolvedSessionLlmCapabilities(capabilities) => {
258 Ok(capabilities)
259 }
260 other => Err(RuntimeDriverError::Internal(format!(
261 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::resolved_session_llm_capabilities: {other:?}"
262 ))),
263 }
264 }
265
266 async fn configure_model_routing_baseline(
267 &self,
268 session_id: &SessionId,
269 baseline_model: meerkat_core::lifecycle::run_primitive::ModelId,
270 realtime_capable: bool,
271 ) -> Result<(), RuntimeDriverError> {
272 match self
273 .execute_meerkat_machine_command(
274 None,
275 MeerkatMachineCommand::ConfigureModelRoutingBaseline {
276 session_id: session_id.clone(),
277 baseline_model,
278 realtime_capable,
279 },
280 )
281 .await
282 .map_err(MeerkatMachine::driver_error_from_command_error)?
283 {
284 MeerkatMachineCommandResult::Unit => Ok(()),
285 other => Err(RuntimeDriverError::Internal(format!(
286 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::configure_model_routing_baseline: {other:?}"
287 ))),
288 }
289 }
290
291 async fn session_model_routing_status(
292 &self,
293 session_id: &SessionId,
294 ) -> Result<meerkat_core::image_generation::SessionModelRoutingStatus, RuntimeDriverError> {
295 match self
296 .execute_meerkat_machine_command(
297 None,
298 MeerkatMachineCommand::SessionModelRoutingStatus {
299 session_id: session_id.clone(),
300 },
301 )
302 .await
303 .map_err(MeerkatMachine::driver_error_from_command_error)?
304 {
305 MeerkatMachineCommandResult::SessionModelRoutingStatus(status) => Ok(status),
306 other => Err(RuntimeDriverError::Internal(format!(
307 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::session_model_routing_status: {other:?}"
308 ))),
309 }
310 }
311
312 async fn request_switch_turn(
313 &self,
314 session_id: &SessionId,
315 request: crate::meerkat_machine_types::SwitchTurnRequest,
316 ) -> Result<meerkat_core::image_generation::SwitchTurnControlResult, RuntimeDriverError> {
317 match self
318 .execute_meerkat_machine_command(
319 None,
320 MeerkatMachineCommand::RequestSwitchTurn {
321 session_id: session_id.clone(),
322 request: Box::new(request),
323 },
324 )
325 .await
326 .map_err(MeerkatMachine::driver_error_from_command_error)?
327 {
328 MeerkatMachineCommandResult::SwitchTurnControlResult(result) => Ok(result),
329 other => Err(RuntimeDriverError::Internal(format!(
330 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::request_switch_turn: {other:?}"
331 ))),
332 }
333 }
334
335 async fn admit_model_routing_assistant_turn(
336 &self,
337 session_id: &SessionId,
338 ) -> Result<(), RuntimeDriverError> {
339 match self
340 .execute_meerkat_machine_command(
341 None,
342 MeerkatMachineCommand::AdmitModelRoutingAssistantTurn {
343 session_id: session_id.clone(),
344 },
345 )
346 .await
347 .map_err(MeerkatMachine::driver_error_from_command_error)?
348 {
349 MeerkatMachineCommandResult::Unit => Ok(()),
350 other => Err(RuntimeDriverError::Internal(format!(
351 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::admit_model_routing_assistant_turn: {other:?}"
352 ))),
353 }
354 }
355
356 async fn begin_image_operation(
357 &self,
358 session_id: &SessionId,
359 request: crate::meerkat_machine_types::ImageOperationRoutingRequest,
360 ) -> Result<crate::meerkat_machine_types::ImageOperationRoutingResult, RuntimeDriverError> {
361 match self
362 .execute_meerkat_machine_command(
363 None,
364 MeerkatMachineCommand::BeginImageOperation {
365 session_id: session_id.clone(),
366 request: Box::new(request),
367 },
368 )
369 .await
370 .map_err(MeerkatMachine::driver_error_from_command_error)?
371 {
372 MeerkatMachineCommandResult::ImageOperationRoutingResult(result) => Ok(result),
373 other => Err(RuntimeDriverError::Internal(format!(
374 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::begin_image_operation: {other:?}"
375 ))),
376 }
377 }
378
379 async fn deny_image_operation_plan(
380 &self,
381 session_id: &SessionId,
382 operation_id: meerkat_core::image_generation::ImageOperationId,
383 reason: meerkat_core::image_generation::ImageOperationDenialReason,
384 ) -> Result<meerkat_core::image_generation::ImageOperationPhase, RuntimeDriverError> {
385 match self
386 .execute_meerkat_machine_command(
387 None,
388 MeerkatMachineCommand::DenyImageOperationPlan {
389 session_id: session_id.clone(),
390 operation_id,
391 reason,
392 },
393 )
394 .await
395 .map_err(MeerkatMachine::driver_error_from_command_error)?
396 {
397 MeerkatMachineCommandResult::ImageOperationPhase(phase) => Ok(phase),
398 other => Err(RuntimeDriverError::Internal(format!(
399 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::deny_image_operation_plan: {other:?}"
400 ))),
401 }
402 }
403
404 async fn activate_image_operation_override(
405 &self,
406 session_id: &SessionId,
407 operation_id: meerkat_core::image_generation::ImageOperationId,
408 ) -> Result<meerkat_core::image_generation::ImageOperationPhase, RuntimeDriverError> {
409 match self
410 .execute_meerkat_machine_command(
411 None,
412 MeerkatMachineCommand::ActivateImageOperationOverride {
413 session_id: session_id.clone(),
414 operation_id,
415 },
416 )
417 .await
418 .map_err(MeerkatMachine::driver_error_from_command_error)?
419 {
420 MeerkatMachineCommandResult::ImageOperationPhase(phase) => Ok(phase),
421 other => Err(RuntimeDriverError::Internal(format!(
422 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::activate_image_operation_override: {other:?}"
423 ))),
424 }
425 }
426
427 async fn complete_image_operation(
428 &self,
429 session_id: &SessionId,
430 operation_id: meerkat_core::image_generation::ImageOperationId,
431 terminal: meerkat_core::image_generation::ImageOperationTerminalClass,
432 ) -> Result<meerkat_core::image_generation::ImageOperationPhase, RuntimeDriverError> {
433 match self
434 .execute_meerkat_machine_command(
435 None,
436 MeerkatMachineCommand::CompleteImageOperation {
437 session_id: session_id.clone(),
438 operation_id,
439 terminal,
440 },
441 )
442 .await
443 .map_err(MeerkatMachine::driver_error_from_command_error)?
444 {
445 MeerkatMachineCommandResult::ImageOperationPhase(phase) => Ok(phase),
446 other => Err(RuntimeDriverError::Internal(format!(
447 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::complete_image_operation: {other:?}"
448 ))),
449 }
450 }
451
452 async fn classify_image_operation_terminal(
453 &self,
454 session_id: &SessionId,
455 operation_id: meerkat_core::image_generation::ImageOperationId,
456 observation: meerkat_core::image_generation::ImageProviderTerminalObservation,
457 provider_text: meerkat_core::image_generation::ProviderTextDisposition,
458 ) -> Result<meerkat_core::image_generation::ImageOperationTerminalClass, RuntimeDriverError>
459 {
460 match self
461 .execute_meerkat_machine_command(
462 None,
463 MeerkatMachineCommand::ClassifyImageOperationTerminal {
464 session_id: session_id.clone(),
465 operation_id,
466 observation,
467 provider_text,
468 },
469 )
470 .await
471 .map_err(MeerkatMachine::driver_error_from_command_error)?
472 {
473 MeerkatMachineCommandResult::ImageOperationTerminalClass(terminal) => Ok(terminal),
474 other => Err(RuntimeDriverError::Internal(format!(
475 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::classify_image_operation_terminal: {other:?}"
476 ))),
477 }
478 }
479
480 async fn restore_image_operation_override(
481 &self,
482 session_id: &SessionId,
483 operation_id: meerkat_core::image_generation::ImageOperationId,
484 ) -> Result<meerkat_core::image_generation::ImageOperationPhase, RuntimeDriverError> {
485 match self
486 .execute_meerkat_machine_command(
487 None,
488 MeerkatMachineCommand::RestoreImageOperationOverride {
489 session_id: session_id.clone(),
490 operation_id,
491 },
492 )
493 .await
494 .map_err(MeerkatMachine::driver_error_from_command_error)?
495 {
496 MeerkatMachineCommandResult::ImageOperationPhase(phase) => Ok(phase),
497 other => Err(RuntimeDriverError::Internal(format!(
498 "unexpected MeerkatMachineCommandResult for SessionServiceRuntimeExt::restore_image_operation_override: {other:?}"
499 ))),
500 }
501 }
502}
503
504impl MeerkatMachine {
509 pub(crate) fn logical_runtime_id(session_id: &SessionId) -> LogicalRuntimeId {
510 LogicalRuntimeId::for_session(session_id)
511 }
512
513 pub(super) fn post_admission_signal_from_effects(
514 effects: &[crate::meerkat_machine::dsl::MeerkatMachineEffect],
515 ) -> crate::driver::ephemeral::PostAdmissionSignal {
516 effects
517 .iter()
518 .find_map(|effect| match effect {
519 crate::meerkat_machine::dsl::MeerkatMachineEffect::PostAdmissionSignal {
520 signal,
521 } => Some(match signal {
522 crate::meerkat_machine::dsl::PostAdmissionSignalKind::WakeLoop => {
523 crate::driver::ephemeral::PostAdmissionSignal::WakeLoop
524 }
525 crate::meerkat_machine::dsl::PostAdmissionSignalKind::InterruptYielding => {
526 crate::driver::ephemeral::PostAdmissionSignal::InterruptYielding
527 }
528 crate::meerkat_machine::dsl::PostAdmissionSignalKind::RequestImmediateProcessing => {
529 crate::driver::ephemeral::PostAdmissionSignal::RequestImmediateProcessing
530 }
531 }),
532 _ => None,
533 })
534 .unwrap_or(crate::driver::ephemeral::PostAdmissionSignal::None)
535 }
536
537 pub(super) fn driver_error_from_command_error(
538 err: MeerkatMachineCommandError,
539 ) -> RuntimeDriverError {
540 match err {
541 MeerkatMachineCommandError::Driver(err) => err,
542 MeerkatMachineCommandError::Control(err) => {
543 Self::driver_error_from_control_plane_error(err)
544 }
545 }
546 }
547
548 pub(super) fn control_plane_error_from_command_error(
549 err: MeerkatMachineCommandError,
550 ) -> RuntimeControlPlaneError {
551 match err {
552 MeerkatMachineCommandError::Control(err) => err,
553 MeerkatMachineCommandError::Driver(err) => {
554 RuntimeControlPlaneError::Internal(err.to_string())
555 }
556 }
557 }
558
559 pub(super) fn driver_error_from_control_plane_error(
560 err: RuntimeControlPlaneError,
561 ) -> RuntimeDriverError {
562 match err {
563 RuntimeControlPlaneError::NotFound(runtime_id) => {
564 RuntimeDriverError::NotFound { runtime_id }
565 }
566 RuntimeControlPlaneError::InvalidState { state } => {
567 RuntimeDriverError::NotReady { state }
568 }
569 RuntimeControlPlaneError::StoreError(message)
570 | RuntimeControlPlaneError::Internal(message) => RuntimeDriverError::Internal(message),
571 }
572 }
573
574 pub(super) async fn resolve_session_id(
576 &self,
577 runtime_id: &LogicalRuntimeId,
578 ) -> Result<SessionId, RuntimeControlPlaneError> {
579 let sessions = self.sessions.read().await;
580 sessions
581 .iter()
582 .find_map(|(session_id, entry)| {
583 (&entry.runtime_id == runtime_id).then(|| session_id.clone())
584 })
585 .ok_or_else(|| RuntimeControlPlaneError::NotFound(runtime_id.clone()))
586 }
587
588 pub(super) async fn existing_session_runtime_state(
589 &self,
590 session_id: &SessionId,
591 ) -> Option<RuntimeState> {
592 let sessions = self.sessions.read().await;
593 let entry = sessions.get(session_id)?;
594 let control = entry.control_snapshot();
599 let authority = entry
600 .dsl_authority
601 .lock()
602 .unwrap_or_else(std::sync::PoisonError::into_inner);
603 let dsl_phase = dsl_authority::runtime_phase_from_authority(&authority);
604 let dsl_pre_run_phase = dsl_authority::pre_run_phase_from_authority(&authority);
605 match crate::meerkat_machine::resolve_visible_runtime_phase(
612 dsl_phase,
613 dsl_pre_run_phase,
614 control.phase,
615 control.pre_run_phase,
616 self.has_runtime_persistence(),
617 ) {
618 Ok(plan) => Some(plan.selected_raw_phase),
619 Err(reason) => {
620 tracing::error!(%session_id, %reason, "MeerkatMachine visible runtime phase resolution failed; failing closed to Destroyed");
621 Some(RuntimeState::Destroyed)
622 }
623 }
624 }
625
626 pub(super) async fn existing_session_visible_runtime_state(
627 &self,
628 session_id: &SessionId,
629 ) -> Option<RuntimeState> {
630 let sessions = self.sessions.read().await;
631 let entry = sessions.get(session_id)?;
632 let control = entry.control_snapshot();
633 let authority = entry
634 .dsl_authority
635 .lock()
636 .unwrap_or_else(std::sync::PoisonError::into_inner);
637 let dsl_phase = dsl_authority::runtime_phase_from_authority(&authority);
638 let dsl_pre_run_phase = dsl_authority::pre_run_phase_from_authority(&authority);
639 match crate::meerkat_machine::resolve_visible_runtime_phase(
645 dsl_phase,
646 dsl_pre_run_phase,
647 control.phase,
648 control.pre_run_phase,
649 self.has_runtime_persistence(),
650 ) {
651 Ok(plan) => Some(plan.visible_phase),
652 Err(reason) => {
653 tracing::error!(%session_id, %reason, "MeerkatMachine visible runtime phase resolution failed; failing closed to Destroyed");
654 Some(RuntimeState::Destroyed)
655 }
656 }
657 }
658
659 pub(super) async fn lookup_entry(
662 &self,
663 runtime_id: &LogicalRuntimeId,
664 ) -> Result<
665 (
666 SessionId,
667 SharedDriver,
668 SharedCompletionRegistry,
669 Option<mpsc::Sender<()>>,
670 ),
671 RuntimeControlPlaneError,
672 > {
673 let sessions = self.sessions.read().await;
674 let (session_id, entry) = sessions
675 .iter()
676 .find(|(_, entry)| &entry.runtime_id == runtime_id)
677 .ok_or_else(|| RuntimeControlPlaneError::NotFound(runtime_id.clone()))?;
678 Ok((
679 session_id.clone(),
680 entry.driver.clone(),
681 entry.completions.clone(),
682 entry.wake_sender(),
683 ))
684 }
685
686 pub async fn retire_runtime_control_plane(
687 &self,
688 runtime_id: &LogicalRuntimeId,
689 ) -> Result<RetireReport, RuntimeControlPlaneError> {
690 tracing::info!(
691 runtime_id = %runtime_id,
692 "MeerkatMachine::retire_runtime_control_plane start"
693 );
694 let (session_id, driver, completions, wake_tx) = self.lookup_entry(runtime_id).await?;
695 let gate = self.session_mutation_gate(&session_id).await;
696 let _gate_guard = match gate {
703 Some(ref gate) => Some(
704 crate::tokio::time::timeout(
705 std::time::Duration::from_secs(30),
706 gate.lock(),
707 )
708 .await
709 .map_err(|_| {
710 RuntimeControlPlaneError::Internal(format!(
711 "retire for session {session_id} timed out acquiring the session mutation gate after 30s; the gate holder is likely deadlocked (stop-under-gate class) — retire can be retried"
712 ))
713 })?,
714 ),
715 None => None,
716 };
717
718 let staged_dsl = self
719 .stage_session_dsl_transition(
720 &session_id,
721 crate::meerkat_machine::dsl::MeerkatMachineInput::Retire {
722 session_id: crate::meerkat_machine::dsl::SessionId::from_domain(&session_id),
723 },
724 "Retire",
725 )
726 .await
727 .map_err(RuntimeControlPlaneError::Internal)?;
728
729 let mut drv = driver.lock().await;
730 let mut report = match Box::pin(machine_retire(&mut drv)).await {
731 Ok(report) => report,
732 Err(err) => {
733 drv.sync_control_projection_from_dsl_authority();
734 return Err(RuntimeControlPlaneError::Internal(err.to_string()));
735 }
736 };
737 drop(drv);
738
739 let mut commit_error = None;
740 if let Err(reason) = self
741 .commit_session_dsl_transition_preserving_committed_state(
742 &session_id,
743 staged_dsl,
744 "Retire",
745 )
746 .await
747 {
748 driver
749 .lock()
750 .await
751 .sync_control_projection_from_dsl_authority();
752 commit_error = Some(reason);
753 }
754
755 if report.inputs_pending_drain > 0 {
756 if let Some(ref tx) = wake_tx
757 && tx.send(()).await.is_ok()
758 {
759 if let Some(reason) = commit_error {
760 return Err(RuntimeControlPlaneError::Internal(reason));
761 }
762 return Ok(report);
763 }
764
765 let mut drv = driver.lock().await;
766 let abandoned = drv
767 .abandon_pending_inputs(crate::input_state::InputAbandonReason::Retired)
768 .await
769 .map_err(|err| RuntimeControlPlaneError::Internal(err.to_string()))?;
770 drop(drv);
771 let result_class =
772 crate::meerkat_machine::driver::machine_resolve_runtime_terminated_completion_result(
773 &driver,
774 )
775 .await
776 .map_err(|err| RuntimeControlPlaneError::Internal(err.to_string()))?;
777 let mut comp = completions.lock().await;
778 comp.resolve_all_runtime_terminated("retired without runtime loop", result_class);
779 report.inputs_abandoned += abandoned;
780 report.inputs_pending_drain = 0;
781 }
782 if let Some(reason) = commit_error {
783 return Err(RuntimeControlPlaneError::Internal(reason));
784 }
785 Ok(report)
786 }
787}
788
789#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
790#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
791impl crate::traits::RuntimeControlPlane for MeerkatMachine {
792 async fn ingest(
793 &self,
794 runtime_id: &LogicalRuntimeId,
795 input: Input,
796 ) -> Result<AcceptOutcome, RuntimeControlPlaneError> {
797 match self
798 .execute_meerkat_machine_command(
799 None,
800 MeerkatMachineCommand::Ingest {
801 runtime_id: runtime_id.clone(),
802 input,
803 },
804 )
805 .await
806 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
807 {
808 MeerkatMachineCommandResult::AcceptOutcome(outcome) => Ok(outcome),
809 other => Err(RuntimeControlPlaneError::Internal(format!(
810 "unexpected MeerkatMachineCommandResult for ingest: {other:?}"
811 ))),
812 }
813 }
814
815 async fn publish_event(
816 &self,
817 event: crate::runtime_event::RuntimeEventEnvelope,
818 ) -> Result<(), RuntimeControlPlaneError> {
819 match self
820 .execute_meerkat_machine_command(None, MeerkatMachineCommand::PublishEvent { event })
821 .await
822 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
823 {
824 MeerkatMachineCommandResult::Unit => Ok(()),
825 other => Err(RuntimeControlPlaneError::Internal(format!(
826 "unexpected MeerkatMachineCommandResult for publish_event: {other:?}"
827 ))),
828 }
829 }
830
831 async fn retire(
832 &self,
833 runtime_id: &LogicalRuntimeId,
834 ) -> Result<RetireReport, RuntimeControlPlaneError> {
835 self.retire_runtime_control_plane(runtime_id).await
836 }
837
838 async fn recycle(
839 &self,
840 runtime_id: &LogicalRuntimeId,
841 ) -> Result<RecycleReport, RuntimeControlPlaneError> {
842 match self
843 .execute_meerkat_machine_command(
844 None,
845 MeerkatMachineCommand::Recycle {
846 runtime_id: runtime_id.clone(),
847 },
848 )
849 .await
850 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
851 {
852 MeerkatMachineCommandResult::RecycleReport(report) => Ok(report),
853 other => Err(RuntimeControlPlaneError::Internal(format!(
854 "unexpected MeerkatMachineCommandResult for recycle: {other:?}"
855 ))),
856 }
857 }
858
859 async fn reset(
860 &self,
861 runtime_id: &LogicalRuntimeId,
862 ) -> Result<crate::traits::ResetReport, RuntimeControlPlaneError> {
863 match self
864 .execute_meerkat_machine_command(
865 None,
866 MeerkatMachineCommand::Reset {
867 runtime_id: runtime_id.clone(),
868 },
869 )
870 .await
871 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
872 {
873 MeerkatMachineCommandResult::ResetReport(report) => Ok(report),
874 other => Err(RuntimeControlPlaneError::Internal(format!(
875 "unexpected MeerkatMachineCommandResult for reset: {other:?}"
876 ))),
877 }
878 }
879
880 async fn recover(
881 &self,
882 runtime_id: &LogicalRuntimeId,
883 ) -> Result<RecoveryReport, RuntimeControlPlaneError> {
884 match self
885 .execute_meerkat_machine_command(
886 None,
887 MeerkatMachineCommand::Recover {
888 runtime_id: runtime_id.clone(),
889 },
890 )
891 .await
892 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
893 {
894 MeerkatMachineCommandResult::RecoveryReport(report) => Ok(report),
895 other => Err(RuntimeControlPlaneError::Internal(format!(
896 "unexpected MeerkatMachineCommandResult for recover: {other:?}"
897 ))),
898 }
899 }
900
901 async fn destroy(
902 &self,
903 runtime_id: &LogicalRuntimeId,
904 ) -> Result<DestroyReport, RuntimeControlPlaneError> {
905 match self
906 .execute_meerkat_machine_command(
907 None,
908 MeerkatMachineCommand::Destroy {
909 runtime_id: runtime_id.clone(),
910 },
911 )
912 .await
913 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
914 {
915 MeerkatMachineCommandResult::DestroyReport(report) => Ok(report),
916 other => Err(RuntimeControlPlaneError::Internal(format!(
917 "unexpected MeerkatMachineCommandResult for destroy: {other:?}"
918 ))),
919 }
920 }
921
922 async fn runtime_state(
923 &self,
924 runtime_id: &LogicalRuntimeId,
925 ) -> Result<RuntimeState, RuntimeControlPlaneError> {
926 match self
927 .execute_meerkat_machine_command(
928 None,
929 MeerkatMachineCommand::RuntimeState {
930 runtime_id: runtime_id.clone(),
931 },
932 )
933 .await
934 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
935 {
936 MeerkatMachineCommandResult::RuntimeState(state) => Ok(state),
937 other => Err(RuntimeControlPlaneError::Internal(format!(
938 "unexpected MeerkatMachineCommandResult for runtime_state: {other:?}"
939 ))),
940 }
941 }
942
943 async fn load_boundary_receipt(
944 &self,
945 runtime_id: &LogicalRuntimeId,
946 run_id: &RunId,
947 sequence: u64,
948 ) -> Result<Option<meerkat_core::lifecycle::RunBoundaryReceipt>, RuntimeControlPlaneError> {
949 match self
950 .execute_meerkat_machine_command(
951 None,
952 MeerkatMachineCommand::LoadBoundaryReceipt {
953 runtime_id: runtime_id.clone(),
954 run_id: run_id.clone(),
955 sequence,
956 },
957 )
958 .await
959 .map_err(MeerkatMachine::control_plane_error_from_command_error)?
960 {
961 MeerkatMachineCommandResult::BoundaryReceipt(receipt) => Ok(receipt),
962 other => Err(RuntimeControlPlaneError::Internal(format!(
963 "unexpected MeerkatMachineCommandResult for load_boundary_receipt: {other:?}"
964 ))),
965 }
966 }
967}
968
969#[cfg(test)]
970#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
971mod tests {
972 use super::*;
973
974 #[test]
979 fn control_plane_not_found_maps_to_driver_not_found() {
980 let runtime_id = LogicalRuntimeId("missing-runtime".to_string());
981 let mapped = MeerkatMachine::driver_error_from_control_plane_error(
982 RuntimeControlPlaneError::NotFound(runtime_id.clone()),
983 );
984
985 match mapped {
986 RuntimeDriverError::NotFound {
987 runtime_id: mapped_id,
988 } => assert_eq!(mapped_id, runtime_id),
989 other => panic!(
990 "expected RuntimeDriverError::NotFound, got {other:?} (must not collapse absence into NotReady/Destroyed)"
991 ),
992 }
993 }
994
995 #[test]
998 fn control_plane_not_found_is_not_destroyed_not_ready() {
999 let mapped = MeerkatMachine::driver_error_from_control_plane_error(
1000 RuntimeControlPlaneError::NotFound(LogicalRuntimeId("missing-runtime".to_string())),
1001 );
1002
1003 assert!(
1004 !matches!(
1005 mapped,
1006 RuntimeDriverError::NotReady {
1007 state: RuntimeState::Destroyed
1008 }
1009 ),
1010 "not-found must not be laundered into NotReady{{Destroyed}}"
1011 );
1012 }
1013}