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