1use std::sync::Arc;
8use std::sync::RwLock as StdRwLock;
9
10use meerkat_core::BlobStore;
11use meerkat_core::lifecycle::{InputId, RunBoundaryReceipt, RunId};
12
13use crate::accept::AcceptOutcome;
14use crate::identifiers::LogicalRuntimeId;
15use crate::input::{Input, externalize_input_images};
16use crate::input_state::{
17 InputAbandonReason, InputLifecycleState, InputState, InputStatePersistenceRecord,
18 StoredInputState,
19};
20use crate::runtime_event::RuntimeEventEnvelope;
21use crate::runtime_state::RuntimeState;
22use crate::store::{
23 FencedInputStateBatchCasOutcome, InputStateBatchCasOutcome, MachineLifecycleCommit,
24 RuntimeStore, RuntimeStoreWriteFence,
25};
26use crate::traits::{DestroyReport, RecoveryReport, RuntimeDriver, RuntimeDriverError};
27
28use super::ephemeral::{
29 EphemeralDriverRollbackSnapshot, EphemeralRuntimeDriver, SharedIngressDslAuthority,
30};
31
32pub struct PersistentRuntimeDriver {
34 inner: EphemeralRuntimeDriver,
36 store: Arc<dyn RuntimeStore>,
38 blob_store: Arc<dyn BlobStore>,
40 runtime_id: LogicalRuntimeId,
42 #[cfg(test)]
46 pub(crate) force_input_snapshot_failure_for_test: bool,
47}
48
49impl PersistentRuntimeDriver {
50 pub(crate) async fn recover_inputs_after_runtime_authority(
51 &mut self,
52 recovered_unregister_progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
53 ) -> Result<RecoveryReport, RuntimeDriverError> {
54 let report = crate::meerkat_machine::machine_recover_persistent_inputs(
55 self.store.as_ref(),
56 &self.runtime_id,
57 &mut self.inner,
58 recovered_unregister_progress,
59 )
60 .await?;
61
62 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
63 self.store
64 .persist_input_states_atomically(&self.runtime_id, &input_states)
65 .await
66 .map_err(|err| {
67 RuntimeDriverError::Internal(format!("recovered input persistence failed: {err}"))
68 })?;
69 Ok(report)
70 }
71
72 pub(crate) async fn recover_inputs_after_runtime_authority_with_fence(
76 &mut self,
77 recovered_unregister_progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
78 write_fence: Arc<dyn RuntimeStoreWriteFence>,
79 ) -> Result<RecoveryReport, RuntimeDriverError> {
80 let observed = self
81 .store
82 .load_input_states(&self.runtime_id)
83 .await
84 .map_err(|error| RuntimeDriverError::RecoveryBackoff {
85 reason: format!("failed to observe durable inputs for recovery: {error}"),
86 })?;
87 let report = crate::meerkat_machine::machine_recover_persistent_inputs_from_observed(
88 self.store.as_ref(),
89 &self.runtime_id,
90 &mut self.inner,
91 observed.clone(),
92 recovered_unregister_progress,
93 )
94 .await?;
95
96 let replacements = self.inner.authorized_stored_input_states_snapshot()?;
97 if replacements.is_empty() {
98 return Ok(report);
99 }
100 let mut expected = Vec::with_capacity(replacements.len());
101 for replacement in &replacements {
102 let input_id = &replacement.as_stored().state.input_id;
103 let Some(original) = observed
104 .iter()
105 .find(|candidate| &candidate.state.input_id == input_id)
106 else {
107 return Err(RuntimeDriverError::RecoveryCorruption {
108 reason: format!(
109 "recovered input {input_id} has no matching durable observation"
110 ),
111 });
112 };
113 expected.push(original.clone());
114 }
115
116 let outcome = self
117 .store
118 .compare_and_swap_input_states_atomically_with_fence(
119 &self.runtime_id,
120 &expected,
121 &replacements,
122 write_fence,
123 )
124 .await
125 .map_err(|error| match error {
126 crate::store::RuntimeStoreError::Unsupported(reason) => {
127 RuntimeDriverError::RecoveryRepairBlocked {
128 evidence_digest: None,
129 reason: format!(
130 "runtime store lacks fenced input recovery capability: {reason}"
131 ),
132 }
133 }
134 other => RuntimeDriverError::RecoveryBackoff {
135 reason: format!("fenced recovered input persistence failed: {other}"),
136 },
137 })?;
138 match outcome {
139 FencedInputStateBatchCasOutcome::Swapped => Ok(report),
140 FencedInputStateBatchCasOutcome::Stale => Err(RuntimeDriverError::StaleAuthority {
141 reason: "durable input state changed while cold recovery was preparing".to_string(),
142 }),
143 FencedInputStateBatchCasOutcome::FenceConflict { reason } => {
144 Err(RuntimeDriverError::StaleAuthority { reason })
145 }
146 FencedInputStateBatchCasOutcome::FenceBackoff { reason } => {
147 Err(RuntimeDriverError::RecoveryBackoff { reason })
148 }
149 }
150 }
151
152 pub fn new(
154 runtime_id: LogicalRuntimeId,
155 store: Arc<dyn RuntimeStore>,
156 blob_store: Arc<dyn BlobStore>,
157 ) -> Self {
158 Self::new_with_control(
159 runtime_id,
160 store,
161 blob_store,
162 Arc::new(StdRwLock::new(
163 crate::driver::ephemeral::RuntimeControlProjection::default(),
164 )),
165 crate::driver::ephemeral::new_ingress_dsl_authority(),
166 )
167 }
168
169 pub(crate) fn new_with_control(
170 runtime_id: LogicalRuntimeId,
171 store: Arc<dyn RuntimeStore>,
172 blob_store: Arc<dyn BlobStore>,
173 control: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
174 dsl: SharedIngressDslAuthority,
175 ) -> Self {
176 Self {
177 inner: EphemeralRuntimeDriver::new_with_control_and_dsl(
178 runtime_id.clone(),
179 control,
180 dsl,
181 ),
182 store,
183 blob_store,
184 runtime_id,
185 #[cfg(test)]
186 force_input_snapshot_failure_for_test: false,
187 }
188 }
189
190 pub fn inner_ref(&self) -> &EphemeralRuntimeDriver {
192 &self.inner
193 }
194
195 pub(crate) fn inner_mut(&mut self) -> &mut EphemeralRuntimeDriver {
196 &mut self.inner
197 }
198
199 #[cfg(test)]
200 pub(crate) async fn compare_and_swap_interaction_terminal_outbox_inputs(
201 &self,
202 expected: &[StoredInputState],
203 input_ids: &[InputId],
204 ) -> Result<InputStateBatchCasOutcome, RuntimeDriverError> {
205 let mut replacements = Vec::with_capacity(input_ids.len());
206 for input_id in input_ids {
207 let replacement = self
208 .inner
209 .authorized_stored_input_state(input_id)?
210 .ok_or_else(|| {
211 RuntimeDriverError::Internal(format!(
212 "interaction terminal outbox input {input_id} disappeared before compare-and-swap"
213 ))
214 })?;
215 replacements.push(replacement);
216 }
217 self.store
218 .compare_and_swap_input_states_atomically(&self.runtime_id, expected, &replacements)
219 .await
220 .map_err(|error| {
221 RuntimeDriverError::Internal(format!(
222 "interaction terminal outbox batch compare-and-swap failed: {error}"
223 ))
224 })
225 }
226
227 pub(crate) async fn compare_and_swap_interaction_terminal_outbox_replacements(
228 &self,
229 expected: &[StoredInputState],
230 replacements: &[crate::input_state::InputStatePersistenceRecord],
231 ) -> Result<InputStateBatchCasOutcome, RuntimeDriverError> {
232 self.store
233 .compare_and_swap_input_states_atomically(&self.runtime_id, expected, replacements)
234 .await
235 .map_err(|error| {
236 RuntimeDriverError::Internal(format!(
237 "interaction terminal outbox batch compare-and-swap failed: {error}"
238 ))
239 })
240 }
241
242 pub(crate) async fn committed_session_snapshot_for_terminal_recovery(
243 &self,
244 ) -> Result<Option<Vec<u8>>, RuntimeDriverError> {
245 self.store
246 .load_session_snapshot(&self.runtime_id)
247 .await
248 .map_err(|error| {
249 RuntimeDriverError::Internal(format!(
250 "interaction terminal recovery failed to load committed session snapshot: {error}"
251 ))
252 })
253 }
254
255 pub(crate) async fn durable_input_states_for_terminal_recovery(
256 &self,
257 ) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
258 self.store
259 .load_input_states(&self.runtime_id)
260 .await
261 .map_err(|error| {
262 RuntimeDriverError::Internal(format!(
263 "interaction terminal recovery failed to load durable input states: {error}"
264 ))
265 })
266 }
267
268 pub(crate) fn rollback_snapshot(&self) -> EphemeralDriverRollbackSnapshot {
269 self.inner.rollback_snapshot()
270 }
271
272 pub(crate) fn restore_rollback_snapshot(&mut self, snapshot: EphemeralDriverRollbackSnapshot) {
273 self.inner.restore_rollback_snapshot(snapshot);
274 }
275
276 pub fn runtime_id(&self) -> &LogicalRuntimeId {
278 &self.runtime_id
279 }
280
281 pub(crate) async fn load_pending_compaction_projections(
282 &self,
283 ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeDriverError> {
284 self.store
285 .load_pending_compaction_projections(&self.runtime_id)
286 .await
287 .map_err(|error| {
288 RuntimeDriverError::Internal(format!(
289 "failed to load compaction projection outbox: {error}"
290 ))
291 })
292 }
293
294 pub(crate) async fn mark_compaction_projection_finalized(
295 &self,
296 projection: &meerkat_core::CompactionProjectionId,
297 ) -> Result<(), RuntimeDriverError> {
298 self.store
299 .mark_compaction_projection_finalized(&self.runtime_id, projection)
300 .await
301 .map_err(|error| {
302 RuntimeDriverError::Internal(format!(
303 "failed to finalize compaction projection outbox: {error}"
304 ))
305 })
306 }
307
308 pub(crate) async fn load_compaction_checkpoint_snapshot(
309 &self,
310 ) -> Result<Option<Vec<u8>>, RuntimeDriverError> {
311 self.store
312 .load_session_snapshot(&self.runtime_id)
313 .await
314 .map_err(|error| {
315 RuntimeDriverError::Internal(format!(
316 "failed to load authoritative compaction checkpoint snapshot: {error}"
317 ))
318 })
319 }
320
321 pub(crate) async fn commit_compaction_checkpoint_snapshot(
322 &self,
323 session_snapshot: Vec<u8>,
324 ) -> Result<(), RuntimeDriverError> {
325 self.store
326 .commit_session_snapshot(
327 &self.runtime_id,
328 crate::store::SessionDelta { session_snapshot },
329 )
330 .await
331 .map_err(|error| {
332 RuntimeDriverError::Internal(format!(
333 "failed to prepare authoritative compaction checkpoint snapshot: {error}"
334 ))
335 })
336 }
337
338 pub fn silent_comms_intents(&self) -> Vec<String> {
339 self.inner.silent_comms_intents()
340 }
341
342 pub fn is_idle(&self) -> bool {
344 self.inner.is_idle()
345 }
346
347 fn runtime_state_for_persistence(&self) -> Result<RuntimeState, RuntimeDriverError> {
349 Self::runtime_state_for_persistence_from_inner(&self.inner)
350 }
351
352 fn runtime_state_for_persistence_from_inner(
353 inner: &EphemeralRuntimeDriver,
354 ) -> Result<RuntimeState, RuntimeDriverError> {
355 crate::meerkat_machine::classify_runtime_lifecycle_durable_state_with_pre_run_phase(
356 inner.runtime_state(),
357 inner.pre_run_phase(),
358 )
359 .map_err(|err| {
360 RuntimeDriverError::Internal(format!(
361 "generated runtime lifecycle durability classification failed: {err}"
362 ))
363 })
364 }
365
366 fn lifecycle_commit_for_persistence(
367 &self,
368 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
369 Self::lifecycle_commit_for_persistence_from_inner(&self.inner)
370 }
371
372 fn lifecycle_commit_for_persistence_with_supervisor_authority(
373 &self,
374 supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
375 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
376 Ok(
377 MachineLifecycleCommit::new_with_binding_and_unregister_progress(
378 Self::runtime_state_for_persistence_from_inner(&self.inner)?,
379 self.inner.machine_lifecycle_binding_facts(),
380 supervisor_authority,
381 Self::unregister_progress_for_persistence_from_inner(&self.inner),
382 ),
383 )
384 }
385
386 fn lifecycle_commit_for_persistence_from_inner(
387 inner: &EphemeralRuntimeDriver,
388 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
389 Ok(
390 MachineLifecycleCommit::new_with_binding_and_unregister_progress(
391 Self::runtime_state_for_persistence_from_inner(inner)?,
392 inner.machine_lifecycle_binding_facts(),
393 inner.supervisor_authority_snapshot(),
394 Self::unregister_progress_for_persistence_from_inner(inner),
395 ),
396 )
397 }
398
399 fn lifecycle_commit_for_completed_unregister(
408 &self,
409 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
410 let completed = {
411 let authority = self.inner.shared_dsl_authority();
412 let authority = authority
413 .lock()
414 .unwrap_or_else(std::sync::PoisonError::into_inner);
415 let state = authority.state();
416 state.registration_phase == crate::meerkat_machine::dsl::RegistrationPhase::Draining
417 && state.session_id.is_none()
418 && state.active_runtime_id.is_none()
419 && state.active_fence_token.is_none()
420 && state.active_runtime_generation.is_none()
421 && state.active_runtime_epoch_id.is_none()
422 && !state.unregister_runtime_loop_drain_pending
423 && !state.unregister_comms_drain_exit_pending
424 && !state.unregister_completion_waiter_drain_pending
425 };
426 if !completed {
427 return Err(RuntimeDriverError::Internal(
428 "completed unregister persistence requires the generated final lifecycle image"
429 .to_string(),
430 ));
431 }
432 Ok(
433 MachineLifecycleCommit::new_with_binding_and_unregister_progress(
434 Self::runtime_state_for_persistence_from_inner(&self.inner)?,
435 self.inner.machine_lifecycle_binding_facts(),
436 self.inner.supervisor_authority_snapshot(),
437 None,
438 ),
439 )
440 }
441
442 fn unregister_progress_for_persistence_from_inner(
443 inner: &EphemeralRuntimeDriver,
444 ) -> Option<crate::store::MachineUnregisterProgressSnapshot> {
445 let authority = inner.shared_dsl_authority();
446 let authority = authority
447 .lock()
448 .unwrap_or_else(std::sync::PoisonError::into_inner);
449 let state = authority.state();
450 (state.registration_phase == crate::meerkat_machine::dsl::RegistrationPhase::Draining).then(
451 || {
452 crate::store::MachineUnregisterProgressSnapshot::new(
453 state.unregister_runtime_loop_drain_pending,
454 state.unregister_comms_drain_exit_pending,
455 state.unregister_completion_waiter_drain_pending,
456 state.unregister_runtime_loop_forced_abort,
457 state.unregister_comms_drain_forced_abort,
458 )
459 },
460 )
461 }
462
463 fn lifecycle_persistence_payload_with_rollback(
473 &mut self,
474 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
475 context: &str,
476 ) -> Result<
477 (
478 super::ephemeral::EphemeralDriverRollbackSnapshot,
479 Vec<InputStatePersistenceRecord>,
480 MachineLifecycleCommit,
481 ),
482 RuntimeDriverError,
483 > {
484 let input_states_result = self.inner.authorized_stored_input_states_snapshot();
485 #[cfg(test)]
486 let input_states_result = if self.force_input_snapshot_failure_for_test {
487 Err(RuntimeDriverError::Internal(
488 "forced input-state snapshot failure for checkpoint-restore contract test"
489 .to_string(),
490 ))
491 } else {
492 input_states_result
493 };
494 let input_states = match input_states_result {
495 Ok(input_states) => input_states,
496 Err(err) => {
497 self.inner.restore_rollback_snapshot(checkpoint);
498 return Err(RuntimeDriverError::Internal(format!(
499 "{context} input-state snapshot failed: {err}"
500 )));
501 }
502 };
503 let commit = match self.lifecycle_commit_for_persistence() {
504 Ok(commit) => commit,
505 Err(err) => {
506 self.inner.restore_rollback_snapshot(checkpoint);
507 return Err(RuntimeDriverError::Internal(format!(
508 "{context} lifecycle commit classification failed: {err}"
509 )));
510 }
511 };
512 Ok((checkpoint, input_states, commit))
513 }
514
515 async fn commit_lifecycle_with_rollback(
516 &mut self,
517 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
518 target_state: RuntimeState,
519 context: &str,
520 ) -> Result<(), RuntimeDriverError> {
521 let (checkpoint, input_states, commit) =
526 self.lifecycle_persistence_payload_with_rollback(checkpoint, context)?;
527 let target_durable_state =
528 match crate::meerkat_machine::classify_runtime_lifecycle_durable_state_with_pre_run_phase(
529 target_state,
530 self.inner.pre_run_phase(),
531 ) {
532 Ok(target_durable_state) => target_durable_state,
533 Err(err) => {
534 self.inner.restore_rollback_snapshot(checkpoint);
535 return Err(RuntimeDriverError::Internal(format!(
536 "{context} generated target lifecycle durability classification failed: {err}"
537 )));
538 }
539 };
540 if commit.runtime_state() != target_durable_state {
541 self.inner.restore_rollback_snapshot(checkpoint);
542 return Err(RuntimeDriverError::Internal(format!(
543 "{context} durable persist target {target_durable_state:?} from live {target_state:?} disagreed with generated lifecycle commit {:?}",
544 commit.runtime_state()
545 )));
546 }
547 if let Err(err) = self
548 .store
549 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
550 .await
551 {
552 self.inner.restore_rollback_snapshot(checkpoint);
553 return Err(RuntimeDriverError::Internal(format!(
554 "{context} persist failed: {err}"
555 )));
556 }
557 Ok(())
558 }
559
560 pub(crate) async fn publish_service_turn_terminal(
561 &mut self,
562 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
563 target_state: RuntimeState,
564 session_snapshot: Vec<u8>,
565 receipt: meerkat_core::lifecycle::RunBoundaryReceipt,
566 owner_session_id: meerkat_core::types::SessionId,
567 ) -> Result<(), RuntimeDriverError> {
568 let commit = match self.lifecycle_commit_for_persistence() {
569 Ok(commit) => commit,
570 Err(error) => {
571 self.inner.restore_rollback_snapshot(checkpoint);
572 return Err(RuntimeDriverError::Internal(format!(
573 "service turn terminal receipt lifecycle classification failed: {error}"
574 )));
575 }
576 };
577 let target_durable_state =
578 match crate::meerkat_machine::classify_runtime_lifecycle_durable_state(target_state) {
579 Ok(target_durable_state) => target_durable_state,
580 Err(error) => {
581 self.inner.restore_rollback_snapshot(checkpoint);
582 return Err(RuntimeDriverError::Internal(format!(
583 "service turn terminal receipt target classification failed: {error}"
584 )));
585 }
586 };
587 if commit.runtime_state() != target_durable_state {
588 self.inner.restore_rollback_snapshot(checkpoint);
589 return Err(RuntimeDriverError::Internal(format!(
590 "service turn terminal receipt durable target {target_durable_state:?} disagreed with generated lifecycle {:?}",
591 commit.runtime_state()
592 )));
593 }
594 if let Err(error) = self
595 .store
596 .atomic_apply_with_machine_lifecycle(
597 &self.runtime_id,
598 crate::store::SessionDelta { session_snapshot },
599 receipt,
600 commit,
601 Vec::new(),
602 owner_session_id,
603 )
604 .await
605 {
606 self.inner.restore_rollback_snapshot(checkpoint);
607 return Err(RuntimeDriverError::Internal(format!(
608 "service turn terminal receipt persist failed: {error}"
609 )));
610 }
611 self.inner.sync_control_projection_from_dsl_authority();
612 Ok(())
613 }
614
615 pub(crate) fn set_control_projection(
616 &mut self,
617 next_phase: RuntimeState,
618 current_run_id: Option<RunId>,
619 pre_run_phase: Option<RuntimeState>,
620 ) {
621 self.inner
622 .set_control_projection(next_phase, current_run_id, pre_run_phase);
623 }
624
625 pub(crate) fn sync_control_projection_from_dsl_authority(&mut self) {
630 self.inner.sync_control_projection_from_dsl_authority();
631 }
632
633 pub(crate) async fn persist_current_machine_lifecycle(
634 &mut self,
635 context: &str,
636 ) -> Result<(), RuntimeDriverError> {
637 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
638 let commit = self.lifecycle_commit_for_persistence()?;
639 self.store
640 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
641 .await
642 .map_err(|err| {
643 RuntimeDriverError::Internal(format!("{context} lifecycle persist failed: {err}"))
644 })
645 }
646
647 pub(crate) async fn commit_unregister_finalization(
648 &mut self,
649 context: &str,
650 retired_ops_epoch: &meerkat_core::RuntimeEpochId,
651 authority: crate::meerkat_machine::DeleteOpsFinalizationAuthority,
652 ) -> Result<(), RuntimeDriverError> {
653 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
654 let commit = self.lifecycle_commit_for_completed_unregister()?;
655 let finalization = crate::store::UnregisterFinalizationCommit::new(
656 commit,
657 input_states,
658 retired_ops_epoch.clone(),
659 authority,
660 );
661 self.store
662 .commit_unregister_finalization(&self.runtime_id, finalization)
663 .await
664 .map_err(|err| match err {
665 crate::store::RuntimeStoreError::UnregisterFinalizationOutcomeUnknown(reason) => {
666 RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
667 reason: format!("{context} lifecycle+ops finalization: {reason}"),
668 }
669 }
670 err => RuntimeDriverError::Internal(format!(
671 "{context} lifecycle+ops finalization failed: {err}"
672 )),
673 })
674 }
675
676 pub(crate) async fn persist_completed_unregister_machine_lifecycle(
677 &mut self,
678 context: &str,
679 _authority: crate::meerkat_machine::RetainOpsFinalizationAuthority,
680 ) -> Result<(), RuntimeDriverError> {
681 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
682 let commit = self.lifecycle_commit_for_completed_unregister()?;
683 self.store
684 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
685 .await
686 .map_err(|error| {
687 RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
695 reason: format!(
696 "{context} retained lifecycle finalization acknowledgement unavailable: {error}"
697 ),
698 }
699 })
700 }
701
702 pub(crate) async fn persist_current_machine_lifecycle_with_supervisor_authority(
708 &mut self,
709 context: &str,
710 supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
711 ) -> Result<(), RuntimeDriverError> {
712 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
713 let commit =
714 self.lifecycle_commit_for_persistence_with_supervisor_authority(supervisor_authority)?;
715 self.store
716 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
717 .await
718 .map_err(|err| {
719 RuntimeDriverError::Internal(format!("{context} lifecycle persist failed: {err}"))
720 })
721 }
722
723 #[doc(hidden)]
726 pub fn contract_begin_run_authority(
727 &mut self,
728 run_id: RunId,
729 ) -> Result<(), RuntimeDriverError> {
730 self.inner.contract_begin_run_authority(run_id)
731 }
732
733 pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
735 self.inner.drain_events()
736 }
737
738 pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
740 self.inner.take_post_admission_signal()
741 }
742
743 pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
745 self.inner.post_admission_signal()
746 }
747
748 pub fn take_wake_requested(&mut self) -> bool {
750 self.inner.take_wake_requested()
751 }
752
753 pub fn take_process_requested(&mut self) -> bool {
755 self.inner.take_process_requested()
756 }
757
758 #[cfg(any(test, debug_assertions, feature = "test-support"))]
761 #[doc(hidden)]
762 pub fn contract_dequeue_next_for_recovery_tests(&mut self) -> Option<(InputId, Input)> {
763 self.inner.contract_dequeue_next_for_recovery_tests()
764 }
765
766 pub(crate) fn dequeue_batch_exact(
767 &mut self,
768 batch: &crate::meerkat_machine::driver::AuthorizedRuntimeLoopBatch,
769 ) -> Result<Vec<(InputId, Input)>, RuntimeDriverError> {
770 self.inner.dequeue_batch_exact(batch)
771 }
772
773 pub fn has_queued_input_outside(&self, excluded: &[InputId]) -> bool {
774 self.inner.has_queued_input_outside(excluded)
775 }
776
777 pub(crate) fn defer_queued_inputs_behind_backlog(
778 &mut self,
779 input_ids: &[InputId],
780 ) -> Result<(), RuntimeDriverError> {
781 self.inner.defer_queued_inputs_behind_backlog(input_ids)
782 }
783
784 pub(crate) fn absorb_post_admission_effects(
785 &mut self,
786 effects: &[crate::meerkat_machine::dsl::MeerkatMachineEffect],
787 ) {
788 self.inner.absorb_post_admission_effects(effects);
789 }
790
791 pub(crate) fn resolve_admission(
792 &self,
793 input: &Input,
794 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
795 self.inner.resolve_admission(input)
796 }
797
798 pub(crate) fn resolve_admission_with_active_turn_boundary(
799 &self,
800 input: &Input,
801 active_turn_boundary_available: bool,
802 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
803 self.inner
804 .resolve_admission_with_active_turn_boundary(input, active_turn_boundary_available)
805 }
806
807 pub(crate) fn resolve_admission_without_wake_with_active_turn_boundary(
808 &self,
809 input: &Input,
810 active_turn_boundary_available: bool,
811 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
812 self.inner
813 .resolve_admission_without_wake_with_active_turn_boundary(
814 input,
815 active_turn_boundary_available,
816 )
817 }
818
819 pub(crate) async fn accept_resolved_input(
820 &mut self,
821 input: Input,
822 resolved: crate::accept::ResolvedAdmission,
823 ) -> Result<AcceptOutcome, RuntimeDriverError> {
824 let mut staged = self.inner.clone_with_isolated_dsl_authority();
825 staged.ensure_contract_session_authority()?;
826 let staged_resolved = if resolved.authority().without_wake() {
827 staged.resolve_admission_without_wake_with_active_turn_boundary(
828 &input,
829 resolved.authority().active_turn_boundary_available(),
830 )?
831 } else {
832 staged.resolve_admission_with_active_turn_boundary(
833 &input,
834 resolved.authority().active_turn_boundary_available(),
835 )?
836 };
837 if !resolved.semantically_equivalent_to(&staged_resolved) {
838 return Err(RuntimeDriverError::Internal(format!(
839 "staged admission resolution diverged from preview: preview={resolved:?}, staged={staged_resolved:?}"
840 )));
841 }
842 let flags = staged_resolved.coarse_flags();
843 let staged_outcome = staged
844 .accept_resolved_input(input.clone(), staged_resolved)
845 .await?;
846
847 let AcceptOutcome::Accepted {
848 input_id: staged_input_id,
849 ..
850 } = staged_outcome
851 else {
852 return self.inner.accept_resolved_input(input, resolved).await;
853 };
854
855 staged.machine_apply_accept_with_completion_signal(&staged_input_id, flags)?;
856 let Some(mut staged_bundle) = staged.stored_input_state(&staged_input_id) else {
857 return Err(RuntimeDriverError::Internal(format!(
858 "generated input lifecycle phase missing for accepted input {staged_input_id}"
859 )));
860 };
861 let mut input_for_recovery = input.clone();
862 externalize_input_images(self.blob_store.as_ref(), &mut input_for_recovery)
863 .await
864 .map_err(|err| {
865 RuntimeDriverError::Internal(format!(
866 "failed to externalize runtime input images: {err}"
867 ))
868 })?;
869 staged_bundle.state.persisted_input = Some(input_for_recovery.clone());
870 self.persist_state(&staged_bundle).await?;
871
872 self.inner.ensure_contract_session_authority()?;
873 let mut outcome = self.inner.accept_resolved_input(input, resolved).await?;
874 if let AcceptOutcome::Accepted {
875 ref input_id,
876 ref mut state,
877 ref mut seed,
878 ..
879 } = outcome
880 {
881 if input_id != &staged_input_id {
882 return Err(RuntimeDriverError::Internal(format!(
883 "staged accepted input {staged_input_id} differed from committed input {input_id}"
884 )));
885 }
886 self.inner
887 .machine_apply_accept_with_completion_signal(input_id, flags)?;
888 let Some(mut bundle) = self.inner.stored_input_state(input_id) else {
889 return Err(RuntimeDriverError::Internal(format!(
890 "generated input lifecycle phase missing for accepted input {input_id}"
891 )));
892 };
893 bundle.state.persisted_input = Some(input_for_recovery);
894 self.inner.ledger_mut().accept(bundle.state.clone());
895 *state = bundle.state;
896 *seed = bundle.seed;
897 }
898
899 Ok(outcome)
900 }
901
902 pub(crate) async fn preview_accept_resolved_input(
903 &self,
904 input: Input,
905 resolved: &crate::accept::ResolvedAdmission,
906 ) -> Result<AcceptOutcome, RuntimeDriverError> {
907 let mut staged = self.inner.clone_with_isolated_dsl_authority();
908 staged.ensure_contract_session_authority()?;
909 let staged_resolved = if resolved.authority().without_wake() {
910 staged.resolve_admission_without_wake_with_active_turn_boundary(
911 &input,
912 resolved.authority().active_turn_boundary_available(),
913 )?
914 } else {
915 staged.resolve_admission_with_active_turn_boundary(
916 &input,
917 resolved.authority().active_turn_boundary_available(),
918 )?
919 };
920 if !resolved.semantically_equivalent_to(&staged_resolved) {
921 return Err(RuntimeDriverError::Internal(format!(
922 "staged admission preview diverged from caller resolution: preview={resolved:?}, staged={staged_resolved:?}"
923 )));
924 }
925 staged.accept_resolved_input(input, staged_resolved).await
926 }
927
928 pub(crate) fn machine_realize_authorized_stage_batch(
929 &mut self,
930 authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
931 ) -> Result<(), crate::traits::RuntimeDriverError> {
932 self.inner.machine_realize_authorized_stage_batch(authority)
933 }
934
935 pub fn apply_input(
937 &mut self,
938 input_id: &InputId,
939 run_id: &meerkat_core::lifecycle::RunId,
940 ) -> Result<(), crate::traits::RuntimeDriverError> {
941 self.inner.apply_input(input_id, run_id)
942 }
943
944 pub(crate) fn machine_realize_terminal_failure_applied(
945 &mut self,
946 run_id: &meerkat_core::lifecycle::RunId,
947 input_ids: &[InputId],
948 ) -> Result<(), crate::traits::RuntimeDriverError> {
949 self.inner
950 .machine_realize_terminal_failure_applied(run_id, input_ids)
951 }
952
953 pub fn rollback_staged(
955 &mut self,
956 input_ids: &[InputId],
957 ) -> Result<(), crate::traits::RuntimeDriverError> {
958 self.inner.rollback_staged(input_ids)
959 }
960
961 async fn persist_state(&self, state: &StoredInputState) -> Result<(), RuntimeDriverError> {
962 let state = InputStatePersistenceRecord::from_machine_snapshot(state.clone())
963 .map_err(RuntimeDriverError::Internal)?;
964 self.store
965 .persist_input_state(&self.runtime_id, &state)
966 .await
967 .map_err(|e| RuntimeDriverError::Internal(e.to_string()))
968 }
969
970 pub(crate) async fn abandon_pending_inputs(
971 &mut self,
972 reason: InputAbandonReason,
973 ) -> Result<usize, RuntimeDriverError> {
974 let checkpoint = self.inner.rollback_snapshot();
975 let abandoned = match self.inner.abandon_pending_inputs(reason) {
976 Ok(abandoned) => abandoned,
977 Err(err) => {
978 self.inner.restore_rollback_snapshot(checkpoint);
979 return Err(err);
980 }
981 };
982 let (checkpoint, input_states, commit) =
983 self.lifecycle_persistence_payload_with_rollback(checkpoint, "pending input abandon")?;
984 if let Err(err) = self
985 .store
986 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
987 .await
988 {
989 self.inner.restore_rollback_snapshot(checkpoint);
990 return Err(RuntimeDriverError::Internal(format!(
991 "pending input abandon persist failed: {err}"
992 )));
993 }
994 Ok(abandoned)
995 }
996
997 pub(crate) async fn abandon_queued_input(
998 &mut self,
999 input_id: &meerkat_core::lifecycle::InputId,
1000 reason: InputAbandonReason,
1001 ) -> Result<bool, RuntimeDriverError> {
1002 let checkpoint = self.inner.rollback_snapshot();
1003 let abandoned = match self.inner.abandon_queued_input(input_id, reason) {
1004 Ok(abandoned) => abandoned,
1005 Err(error) => {
1006 self.inner.restore_rollback_snapshot(checkpoint);
1007 return Err(error);
1008 }
1009 };
1010 if !abandoned {
1011 return Ok(false);
1012 }
1013 let (checkpoint, input_states, commit) =
1014 self.lifecycle_persistence_payload_with_rollback(checkpoint, "tracked input cancel")?;
1015 if let Err(error) = self
1016 .store
1017 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1018 .await
1019 {
1020 self.inner.restore_rollback_snapshot(checkpoint);
1021 return Err(RuntimeDriverError::Internal(format!(
1022 "tracked input cancel persist failed: {error}"
1023 )));
1024 }
1025 Ok(true)
1026 }
1027
1028 pub(crate) async fn recycle_preserving_work(&mut self) -> Result<usize, RuntimeDriverError> {
1033 let checkpoint = self.inner.rollback_snapshot();
1034 let transferred = match self.inner.recycle_preserving_work() {
1035 Ok(transferred) => transferred,
1036 Err(err) => {
1037 self.inner.restore_rollback_snapshot(checkpoint);
1038 return Err(err);
1039 }
1040 };
1041 let (checkpoint, input_states, commit) =
1042 self.lifecycle_persistence_payload_with_rollback(checkpoint, "recycle")?;
1043 if let Err(err) = self
1044 .store
1045 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1046 .await
1047 {
1048 self.inner.restore_rollback_snapshot(checkpoint);
1049 return Err(RuntimeDriverError::Internal(format!(
1050 "recycle persist failed: {err}"
1051 )));
1052 }
1053
1054 self.inner.sync_control_projection_from_dsl_authority();
1055 Ok(transferred)
1056 }
1057
1058 pub(crate) async fn realize_retire_lifecycle(
1059 &mut self,
1060 ) -> Result<crate::traits::RetireReport, RuntimeDriverError> {
1061 let checkpoint = self.inner.rollback_snapshot();
1062 let report = self.inner.finalize_retire();
1063 let target_state = match self.runtime_state_for_persistence() {
1066 Ok(target_state) => target_state,
1067 Err(err) => {
1068 self.inner.restore_rollback_snapshot(checkpoint);
1069 return Err(err);
1070 }
1071 };
1072 self.commit_lifecycle_with_rollback(checkpoint, target_state, "retire")
1073 .await?;
1074 self.inner.sync_control_projection_from_dsl_authority();
1075 Ok(report)
1076 }
1077
1078 pub(crate) async fn realize_reset_lifecycle(
1079 &mut self,
1080 ) -> Result<crate::traits::ResetReport, RuntimeDriverError> {
1081 let checkpoint = self.inner.rollback_snapshot();
1082 let report = match self.inner.reset_cleanup() {
1083 Ok(report) => report,
1084 Err(err) => {
1085 self.inner.restore_rollback_snapshot(checkpoint);
1086 return Err(err);
1087 }
1088 };
1089 let target_state = match self.runtime_state_for_persistence() {
1092 Ok(target_state) => target_state,
1093 Err(err) => {
1094 self.inner.restore_rollback_snapshot(checkpoint);
1095 return Err(err);
1096 }
1097 };
1098 self.commit_lifecycle_with_rollback(checkpoint, target_state, "reset")
1099 .await?;
1100 self.inner.sync_control_projection_from_dsl_authority();
1101 Ok(report)
1102 }
1103
1104 pub(crate) fn prepare_destroy_lifecycle(
1105 &mut self,
1106 ) -> Result<(EphemeralDriverRollbackSnapshot, DestroyReport), RuntimeDriverError> {
1107 let checkpoint = self.inner.rollback_snapshot();
1108 let abandoned = match self.inner.destroy_cleanup() {
1109 Ok(abandoned) => abandoned,
1110 Err(err) => {
1111 self.inner.restore_rollback_snapshot(checkpoint);
1112 return Err(err);
1113 }
1114 };
1115 Ok((
1116 checkpoint,
1117 DestroyReport {
1118 inputs_abandoned: abandoned,
1119 },
1120 ))
1121 }
1122
1123 pub(crate) async fn commit_prepared_destroy_lifecycle(
1124 &mut self,
1125 checkpoint: EphemeralDriverRollbackSnapshot,
1126 ) -> Result<(), RuntimeDriverError> {
1127 let target_state = match self.runtime_state_for_persistence() {
1132 Ok(target_state) => target_state,
1133 Err(err) => {
1134 self.inner.restore_rollback_snapshot(checkpoint);
1135 return Err(err);
1136 }
1137 };
1138 self.commit_lifecycle_with_rollback(checkpoint, target_state, "destroy")
1139 .await
1140 }
1141
1142 pub(crate) fn rollback_prepared_destroy_lifecycle(
1143 &mut self,
1144 checkpoint: EphemeralDriverRollbackSnapshot,
1145 ) {
1146 self.inner.restore_rollback_snapshot(checkpoint);
1147 }
1148
1149 pub(crate) async fn finalize_runtime_executor_exit(
1150 &mut self,
1151 ) -> Result<(), RuntimeDriverError> {
1152 let checkpoint = self.inner.rollback_snapshot();
1153 if let Err(err) = self.inner.apply_runtime_executor_exited_authority() {
1154 self.inner.restore_rollback_snapshot(checkpoint);
1155 return Err(err);
1156 }
1157 if let Err(err) = self.inner.stop_runtime_cleanup() {
1158 self.inner.restore_rollback_snapshot(checkpoint);
1159 return Err(err);
1160 }
1161 let target_state = match self.runtime_state_for_persistence() {
1165 Ok(target_state) => target_state,
1166 Err(err) => {
1167 self.inner.restore_rollback_snapshot(checkpoint);
1168 return Err(err);
1169 }
1170 };
1171 self.commit_lifecycle_with_rollback(checkpoint, target_state, "stop")
1172 .await?;
1173 self.inner.sync_control_projection_from_dsl_authority();
1174 Ok(())
1175 }
1176
1177 pub(crate) fn machine_realize_boundary_applied_in_memory(
1178 &mut self,
1179 run_id: &RunId,
1180 receipt: &RunBoundaryReceipt,
1181 ) -> Result<(), RuntimeDriverError> {
1182 self.inner.machine_realize_boundary_applied(run_id, receipt)
1183 }
1184
1185 pub(crate) fn machine_realize_run_completed_in_memory(
1186 &mut self,
1187 run_id: &RunId,
1188 consumed_input_ids: &[InputId],
1189 ) -> Result<(), RuntimeDriverError> {
1190 self.inner
1191 .machine_realize_run_completed(run_id, consumed_input_ids)
1192 }
1193
1194 pub(crate) async fn machine_realize_live_boundary_context_injected(
1195 &mut self,
1196 run_id: &RunId,
1197 input_ids: &[InputId],
1198 stage_authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
1199 session_snapshot: Option<Vec<u8>>,
1200 ) -> Result<(), RuntimeDriverError> {
1201 let checkpoint = self.inner.rollback_snapshot();
1202 let receipt = match self.inner.machine_realize_live_boundary_context_injected(
1203 run_id,
1204 input_ids,
1205 stage_authority,
1206 ) {
1207 Ok(receipt) => receipt,
1208 Err(err) => {
1209 self.inner.restore_rollback_snapshot(checkpoint);
1210 return Err(err);
1211 }
1212 };
1213 let input_updates = match self.inner.authorized_stored_input_states_snapshot() {
1214 Ok(input_updates) => input_updates,
1215 Err(err) => {
1216 self.inner.restore_rollback_snapshot(checkpoint);
1217 return Err(err);
1218 }
1219 };
1220 if let Err(err) = self
1221 .store
1222 .atomic_apply(
1223 &self.runtime_id,
1224 session_snapshot
1225 .as_ref()
1226 .map(|session_snapshot| crate::store::SessionDelta {
1227 session_snapshot: session_snapshot.clone(),
1228 }),
1229 receipt.clone(),
1230 input_updates,
1231 session_snapshot
1232 .as_deref()
1233 .and_then(|snapshot| {
1234 serde_json::from_slice::<meerkat_core::Session>(snapshot).ok()
1235 })
1236 .map(|session| session.id().clone()),
1237 )
1238 .await
1239 {
1240 self.inner.restore_rollback_snapshot(checkpoint);
1241 return Err(RuntimeDriverError::Internal(format!(
1242 "runtime live-boundary context commit failed: {err}"
1243 )));
1244 }
1245 Ok(())
1246 }
1247
1248 pub(crate) async fn machine_commit_completed_boundary_snapshot(
1249 &mut self,
1250 receipt: &RunBoundaryReceipt,
1251 session_snapshot: Option<&Vec<u8>>,
1252 owner_session_id: &meerkat_core::types::SessionId,
1253 ) -> Result<(), RuntimeDriverError> {
1254 let input_updates = self.inner.authorized_stored_input_states_snapshot()?;
1255 self.store
1256 .atomic_apply(
1257 &self.runtime_id,
1258 session_snapshot.map(|session_snapshot| crate::store::SessionDelta {
1259 session_snapshot: session_snapshot.clone(),
1260 }),
1261 receipt.clone(),
1262 input_updates,
1263 Some(owner_session_id.clone()),
1264 )
1265 .await
1266 .map_err(|e| {
1267 RuntimeDriverError::Internal(format!(
1268 "runtime completed-boundary commit failed: {e}"
1269 ))
1270 })
1271 }
1272
1273 pub(crate) async fn persist_machine_realized_run_failed(
1279 &mut self,
1280 realization: crate::meerkat_machine::driver::MachineRunFailureRealization,
1281 ) -> Result<(), RuntimeDriverError> {
1282 let crate::meerkat_machine::driver::MachineRunFailureRealization {
1283 run_id,
1284 contributing_input_ids,
1285 replay_plan,
1286 terminal_error,
1287 runtime_apply_failure,
1288 recoverable,
1289 applied_commit,
1290 } = realization;
1291 let checkpoint = self.inner.rollback_snapshot();
1292 let failure_cause = runtime_apply_failure.as_ref().map(|failure| failure.kind);
1293 tracing::debug!(
1294 run_id = ?run_id,
1295 contributors = contributing_input_ids.len(),
1296 replay_kind = replay_plan.notice_kind,
1297 recoverable,
1298 error = terminal_error,
1299 failure_cause = ?failure_cause,
1300 "persistent driver realized machine-owned failed-run replay"
1301 );
1302 let (checkpoint, input_states, commit) = self
1303 .lifecycle_persistence_payload_with_rollback(checkpoint, "failed-run terminal event")?;
1304 let persist_result = if let Some(applied_commit) = applied_commit {
1305 let session = match serde_json::from_slice::<meerkat_core::Session>(
1306 &applied_commit.session_snapshot,
1307 ) {
1308 Ok(session) => session,
1309 Err(error) => {
1310 self.inner.restore_rollback_snapshot(checkpoint);
1311 return Err(RuntimeDriverError::Internal(format!(
1312 "machine-terminal session snapshot was not a Session: {error}"
1313 )));
1314 }
1315 };
1316 if session.id() != &applied_commit.owner_session_id {
1317 self.inner.restore_rollback_snapshot(checkpoint);
1318 return Err(RuntimeDriverError::Internal(format!(
1319 "machine-terminal session owner changed after validation: generated {}, snapshot {}",
1320 applied_commit.owner_session_id,
1321 session.id()
1322 )));
1323 }
1324 self.store
1325 .atomic_apply_with_machine_lifecycle(
1326 &self.runtime_id,
1327 crate::store::SessionDelta {
1328 session_snapshot: applied_commit.session_snapshot,
1329 },
1330 applied_commit.receipt,
1331 commit,
1332 input_states,
1333 applied_commit.owner_session_id,
1334 )
1335 .await
1336 } else {
1337 self.store
1338 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1339 .await
1340 };
1341 if let Err(err) = persist_result {
1342 self.inner.restore_rollback_snapshot(checkpoint);
1343 return Err(RuntimeDriverError::Internal(format!(
1344 "terminal event persist failed: {err}"
1345 )));
1346 }
1347 Ok(())
1348 }
1349
1350 pub(crate) async fn machine_realize_run_cancelled(
1351 &mut self,
1352 run_id: &RunId,
1353 contributing_input_ids: &[InputId],
1354 ) -> Result<(), RuntimeDriverError> {
1355 let checkpoint = self.inner.rollback_snapshot();
1356 if let Err(err) = self
1357 .inner
1358 .machine_realize_run_cancelled(run_id, contributing_input_ids)
1359 {
1360 self.inner.restore_rollback_snapshot(checkpoint);
1361 return Err(err);
1362 }
1363 tracing::debug!(
1364 run_id = ?run_id,
1365 contributors = contributing_input_ids.len(),
1366 "persistent driver realized machine-owned cancelled run"
1367 );
1368 let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
1369 checkpoint,
1370 "cancelled-run terminal event",
1371 )?;
1372 if let Err(err) = self
1373 .store
1374 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1375 .await
1376 {
1377 self.inner.restore_rollback_snapshot(checkpoint);
1378 return Err(RuntimeDriverError::Internal(format!(
1379 "terminal cancellation persist failed: {err}"
1380 )));
1381 }
1382 Ok(())
1383 }
1384}
1385
1386#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1387#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1388impl RuntimeDriver for PersistentRuntimeDriver {
1389 async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError> {
1390 let resolved = self.resolve_admission(&input)?;
1391 self.accept_resolved_input(input, resolved).await
1392 }
1393
1394 async fn on_runtime_event(
1395 &mut self,
1396 event: RuntimeEventEnvelope,
1397 ) -> Result<(), RuntimeDriverError> {
1398 self.inner.on_runtime_event(event).await
1399 }
1400
1401 async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError> {
1402 let report = crate::meerkat_machine::machine_recover_persistent_driver(
1403 self.store.as_ref(),
1404 &self.runtime_id,
1405 &mut self.inner,
1406 )
1407 .await?;
1408
1409 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
1410 self.store
1411 .persist_input_states_atomically(&self.runtime_id, &input_states)
1412 .await
1413 .map_err(|err| {
1414 RuntimeDriverError::Internal(format!("recovered input persistence failed: {err}"))
1415 })?;
1416 Ok(report)
1417 }
1418
1419 fn runtime_state(&self) -> RuntimeState {
1420 self.inner.runtime_state()
1421 }
1422
1423 fn input_state(&self, input_id: &InputId) -> Option<&InputState> {
1424 self.inner.input_state(input_id)
1425 }
1426
1427 fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState> {
1428 self.inner.input_phase(input_id)
1429 }
1430
1431 fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId> {
1432 self.inner.input_last_run_id(input_id)
1433 }
1434
1435 fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64> {
1436 self.inner.input_last_boundary_sequence(input_id)
1437 }
1438
1439 fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState> {
1440 self.inner.stored_input_state(input_id)
1441 }
1442
1443 fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
1444 self.inner.stored_input_states_snapshot()
1445 }
1446
1447 fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId> {
1448 self.inner.input_id_for_idempotency_key(idempotency_key)
1449 }
1450
1451 fn active_input_ids(&self) -> Vec<InputId> {
1452 self.inner.active_input_ids()
1453 }
1454}
1455
1456#[cfg(test)]
1457#[allow(clippy::unwrap_used, clippy::expect_used)]
1458mod tests {
1459 use super::*;
1460 use chrono::Utc;
1461 use meerkat_core::lifecycle::InputId;
1462
1463 fn make_prompt(text: &str) -> Input {
1464 Input::Prompt(crate::input::PromptInput {
1465 injected_context: Vec::new(),
1466 header: crate::input::InputHeader {
1467 id: InputId::new(),
1468 timestamp: Utc::now(),
1469 source: crate::input::InputOrigin::Operator,
1470 durability: crate::input::InputDurability::Durable,
1471 visibility: crate::input::InputVisibility::default(),
1472 idempotency_key: None,
1473 supersession_key: None,
1474 correlation_id: None,
1475 },
1476 content: text.into(),
1477 typed_turn_appends: Vec::new(),
1478 turn_metadata: None,
1479 })
1480 }
1481
1482 #[tokio::test]
1488 async fn commit_lifecycle_snapshot_failure_restores_checkpoint() {
1489 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1490 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1491 let rid = LogicalRuntimeId::new("commit-lifecycle-rollback-contract");
1492 let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1493
1494 let checkpoint = driver.rollback_snapshot();
1496
1497 let input = make_prompt("staged work");
1500 let input_id = input.id().clone();
1501 let outcome = driver.accept_input(input).await.unwrap();
1502 assert!(outcome.is_accepted());
1503 assert!(driver.input_phase(&input_id).is_some());
1504
1505 driver.force_input_snapshot_failure_for_test = true;
1507 let target_state = driver.inner_ref().runtime_state();
1508 let result = driver
1509 .commit_lifecycle_with_rollback(checkpoint, target_state, "test destroy")
1510 .await;
1511
1512 assert!(result.is_err(), "forced snapshot failure must propagate");
1515 assert!(
1516 driver.input_phase(&input_id).is_none(),
1517 "staged driver state must be restored to the pre-stage checkpoint"
1518 );
1519 assert!(driver.active_input_ids().is_empty());
1520 }
1521
1522 #[tokio::test]
1528 async fn abandon_pending_inputs_snapshot_failure_restores_checkpoint() {
1529 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1530 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1531 let rid = LogicalRuntimeId::new("abandon-rollback-contract");
1532 let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1533
1534 let input = make_prompt("pending work");
1536 let input_id = input.id().clone();
1537 let outcome = driver.accept_input(input).await.unwrap();
1538 assert!(outcome.is_accepted());
1539 assert!(driver.input_phase(&input_id).is_some());
1540
1541 driver.force_input_snapshot_failure_for_test = true;
1544 let result = driver
1545 .abandon_pending_inputs(InputAbandonReason::Reset)
1546 .await;
1547
1548 assert!(result.is_err(), "forced snapshot failure must propagate");
1549 assert!(
1550 driver.input_phase(&input_id).is_some(),
1551 "staged abandon must be rolled back: the pending input must still be live"
1552 );
1553 }
1554
1555 #[tokio::test]
1556 async fn retiring_active_run_persists_retired_before_dropping_live_witness() {
1557 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1558 let runtime_id = LogicalRuntimeId::new("retire-active-run-durability");
1559 let runtime_store: Arc<dyn RuntimeStore> = store.clone();
1560 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1561 let mut driver =
1562 PersistentRuntimeDriver::new(runtime_id.clone(), runtime_store, blob_store);
1563 let run_id = RunId::new();
1564
1565 driver
1566 .contract_begin_run_authority(run_id.clone())
1567 .expect("contract run admission");
1568 assert_eq!(driver.runtime_state(), RuntimeState::Running);
1569 assert_eq!(driver.inner_ref().current_run_id(), Some(run_id));
1570 assert!(driver.inner_ref().pre_run_phase().is_some());
1571
1572 let session_id = driver.inner_ref().session_authority_id_for_recovery();
1573 {
1574 let authority = driver.inner_ref().shared_dsl_authority();
1575 let mut authority = authority
1576 .lock()
1577 .unwrap_or_else(std::sync::PoisonError::into_inner);
1578 crate::meerkat_machine::dsl::MeerkatMachineMutator::apply(
1579 &mut *authority,
1580 crate::meerkat_machine::dsl::MeerkatMachineInput::Retire { session_id },
1581 )
1582 .expect("machine-authorized mid-run retire transition");
1583 }
1584 driver.sync_control_projection_from_dsl_authority();
1585 assert_eq!(driver.runtime_state(), RuntimeState::Retired);
1586 assert!(
1587 driver.inner_ref().pre_run_phase().is_some(),
1588 "Retire commits before the live run witness is dropped"
1589 );
1590
1591 driver
1592 .realize_retire_lifecycle()
1593 .await
1594 .expect("mid-run retire must durably commit");
1595
1596 assert_eq!(driver.runtime_state(), RuntimeState::Retired);
1597 assert_eq!(
1598 crate::store::load_runtime_state(store.as_ref(), &runtime_id)
1599 .await
1600 .expect("reload durable lifecycle"),
1601 Some(RuntimeState::Retired)
1602 );
1603 }
1604
1605 #[tokio::test]
1606 async fn interaction_terminal_outbox_delegator_swaps_exact_rows_and_reports_stale() {
1607 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1608 let store_trait: Arc<dyn RuntimeStore> = store.clone();
1609 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1610 let rid = LogicalRuntimeId::new("interaction-outbox-cas-delegator");
1611 let mut driver = PersistentRuntimeDriver::new(rid.clone(), store_trait, blob_store);
1612
1613 let mut input_ids = Vec::new();
1614 for text in ["first", "second"] {
1615 let input = make_prompt(text);
1616 input_ids.push(input.id().clone());
1617 assert!(driver.accept_input(input).await.unwrap().is_accepted());
1618 }
1619 let expected = store.load_input_states(&rid).await.unwrap();
1625 for input_id in &input_ids {
1626 driver
1627 .inner_mut()
1628 .ledger_mut()
1629 .get_mut(input_id)
1630 .unwrap()
1631 .recovery_count = 1;
1632 }
1633
1634 assert_eq!(
1635 driver
1636 .compare_and_swap_interaction_terminal_outbox_inputs(&expected, &input_ids)
1637 .await
1638 .unwrap(),
1639 InputStateBatchCasOutcome::Swapped
1640 );
1641 assert!(
1642 store
1643 .load_input_states(&rid)
1644 .await
1645 .unwrap()
1646 .iter()
1647 .all(|row| row.state.recovery_count == 1)
1648 );
1649
1650 for input_id in &input_ids {
1651 driver
1652 .inner_mut()
1653 .ledger_mut()
1654 .get_mut(input_id)
1655 .unwrap()
1656 .recovery_count = 2;
1657 }
1658 assert_eq!(
1659 driver
1660 .compare_and_swap_interaction_terminal_outbox_inputs(&expected, &input_ids)
1661 .await
1662 .unwrap(),
1663 InputStateBatchCasOutcome::Stale
1664 );
1665 assert!(
1666 store
1667 .load_input_states(&rid)
1668 .await
1669 .unwrap()
1670 .iter()
1671 .all(|row| row.state.recovery_count == 1),
1672 "a stale delegator CAS must not mutate any durable row"
1673 );
1674 }
1675
1676 #[tokio::test]
1677 async fn recover_atomically_rewrites_cold_running_lifecycle_to_idle() {
1678 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1679 let runtime_id = LogicalRuntimeId::new("rewrite-cold-running-lifecycle");
1680 store
1681 .commit_machine_lifecycle(
1682 &runtime_id,
1683 MachineLifecycleCommit::new_with_binding(
1684 RuntimeState::Running,
1685 crate::store::MachineLifecycleBindingFacts::new(
1686 Some("rt:cold-running".to_string()),
1687 Some(9),
1688 Some(2),
1689 Some("epoch-cold-running".to_string()),
1690 ),
1691 crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
1692 ),
1693 &[],
1694 )
1695 .await
1696 .expect("seed legacy cold Running lifecycle");
1697
1698 let runtime_store: Arc<dyn RuntimeStore> = store.clone();
1699 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1700 let mut driver =
1701 PersistentRuntimeDriver::new(runtime_id.clone(), runtime_store, blob_store);
1702
1703 driver
1704 .recover()
1705 .await
1706 .expect("cold Running recovery should converge durably");
1707
1708 assert_eq!(driver.runtime_state(), RuntimeState::Idle);
1709 assert_eq!(
1710 crate::store::load_runtime_state(store.as_ref(), &runtime_id)
1711 .await
1712 .expect("reload durable lifecycle"),
1713 Some(RuntimeState::Idle),
1714 "recovery acknowledgement must mean the torn lifecycle row is repaired"
1715 );
1716 }
1717
1718 #[tokio::test]
1719 async fn exact_batch_cas_fences_stale_two_handle_finalization_and_publication_writes() {
1720 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1721 let store_trait: Arc<dyn RuntimeStore> = store.clone();
1722 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1723 let rid = LogicalRuntimeId::new("interaction-outbox-two-handle-phase-fence");
1724 let mut owner =
1725 PersistentRuntimeDriver::new(rid.clone(), store_trait.clone(), blob_store.clone());
1726 let mut input_ids = Vec::new();
1727 for text in ["first", "second"] {
1728 let input = make_prompt(text);
1729 input_ids.push(input.id().clone());
1730 assert!(owner.accept_input(input).await.unwrap().is_accepted());
1731 }
1732
1733 let initial = store.load_input_states(&rid).await.unwrap();
1735 for input_id in &input_ids {
1736 owner
1737 .inner_mut()
1738 .ledger_mut()
1739 .get_mut(input_id)
1740 .unwrap()
1741 .recovery_count = 10;
1742 }
1743 assert_eq!(
1744 owner
1745 .compare_and_swap_interaction_terminal_outbox_inputs(&initial, &input_ids)
1746 .await
1747 .unwrap(),
1748 InputStateBatchCasOutcome::Swapped
1749 );
1750 let owner_witness = store.load_input_states(&rid).await.unwrap();
1751
1752 let mut takeover =
1754 PersistentRuntimeDriver::new(rid.clone(), store_trait.clone(), blob_store.clone());
1755 RuntimeDriver::recover(&mut takeover).await.unwrap();
1756 let takeover_expected = store.load_input_states(&rid).await.unwrap();
1757 for input_id in &input_ids {
1758 takeover
1759 .inner_mut()
1760 .ledger_mut()
1761 .get_mut(input_id)
1762 .unwrap()
1763 .recovery_count = 20;
1764 }
1765 assert_eq!(
1766 takeover
1767 .compare_and_swap_interaction_terminal_outbox_inputs(
1768 &takeover_expected,
1769 &input_ids,
1770 )
1771 .await
1772 .unwrap(),
1773 InputStateBatchCasOutcome::Swapped
1774 );
1775 for input_id in &input_ids {
1776 owner
1777 .inner_mut()
1778 .ledger_mut()
1779 .get_mut(input_id)
1780 .unwrap()
1781 .recovery_count = 30;
1782 }
1783 assert_eq!(
1784 owner
1785 .compare_and_swap_interaction_terminal_outbox_inputs(&owner_witness, &input_ids)
1786 .await
1787 .unwrap(),
1788 InputStateBatchCasOutcome::Stale,
1789 "the superseded owner must not overwrite takeover at finalization"
1790 );
1791 assert!(
1792 store
1793 .load_input_states(&rid)
1794 .await
1795 .unwrap()
1796 .iter()
1797 .all(|row| row.state.recovery_count == 20)
1798 );
1799
1800 let takeover_witness = store.load_input_states(&rid).await.unwrap();
1804 for input_id in &input_ids {
1805 takeover
1806 .inner_mut()
1807 .ledger_mut()
1808 .get_mut(input_id)
1809 .unwrap()
1810 .recovery_count = 40;
1811 }
1812 assert_eq!(
1813 takeover
1814 .compare_and_swap_interaction_terminal_outbox_inputs(&takeover_witness, &input_ids,)
1815 .await
1816 .unwrap(),
1817 InputStateBatchCasOutcome::Swapped
1818 );
1819 let finalized_witness = store.load_input_states(&rid).await.unwrap();
1820 let mut publisher = PersistentRuntimeDriver::new(rid.clone(), store_trait, blob_store);
1821 RuntimeDriver::recover(&mut publisher).await.unwrap();
1822 let publisher_expected = store.load_input_states(&rid).await.unwrap();
1823 for input_id in &input_ids {
1824 publisher
1825 .inner_mut()
1826 .ledger_mut()
1827 .get_mut(input_id)
1828 .unwrap()
1829 .recovery_count = 50;
1830 }
1831 assert_eq!(
1832 publisher
1833 .compare_and_swap_interaction_terminal_outbox_inputs(
1834 &publisher_expected,
1835 &input_ids,
1836 )
1837 .await
1838 .unwrap(),
1839 InputStateBatchCasOutcome::Swapped
1840 );
1841 for input_id in &input_ids {
1842 takeover
1843 .inner_mut()
1844 .ledger_mut()
1845 .get_mut(input_id)
1846 .unwrap()
1847 .recovery_count = 60;
1848 }
1849 assert_eq!(
1850 takeover
1851 .compare_and_swap_interaction_terminal_outbox_inputs(
1852 &finalized_witness,
1853 &input_ids,
1854 )
1855 .await
1856 .unwrap(),
1857 InputStateBatchCasOutcome::Stale,
1858 "the superseded finalizer must not overwrite takeover at publication"
1859 );
1860 assert!(
1861 store
1862 .load_input_states(&rid)
1863 .await
1864 .unwrap()
1865 .iter()
1866 .all(|row| row.state.recovery_count == 50)
1867 );
1868 }
1869}