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::{MachineLifecycleCommit, RuntimeStore};
23use crate::traits::{DestroyReport, RecoveryReport, RuntimeDriver, RuntimeDriverError};
24
25use super::ephemeral::{
26 EphemeralDriverRollbackSnapshot, EphemeralRuntimeDriver, SharedIngressDslAuthority,
27};
28
29pub struct PersistentRuntimeDriver {
31 inner: EphemeralRuntimeDriver,
33 store: Arc<dyn RuntimeStore>,
35 blob_store: Arc<dyn BlobStore>,
37 runtime_id: LogicalRuntimeId,
39 #[cfg(test)]
43 pub(crate) force_input_snapshot_failure_for_test: bool,
44}
45
46impl PersistentRuntimeDriver {
47 pub fn new(
49 runtime_id: LogicalRuntimeId,
50 store: Arc<dyn RuntimeStore>,
51 blob_store: Arc<dyn BlobStore>,
52 ) -> Self {
53 Self::new_with_control(
54 runtime_id,
55 store,
56 blob_store,
57 Arc::new(StdRwLock::new(
58 crate::driver::ephemeral::RuntimeControlProjection::default(),
59 )),
60 crate::driver::ephemeral::new_ingress_dsl_authority(),
61 )
62 }
63
64 pub(crate) fn new_with_control(
65 runtime_id: LogicalRuntimeId,
66 store: Arc<dyn RuntimeStore>,
67 blob_store: Arc<dyn BlobStore>,
68 control: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
69 dsl: SharedIngressDslAuthority,
70 ) -> Self {
71 Self {
72 inner: EphemeralRuntimeDriver::new_with_control_and_dsl(
73 runtime_id.clone(),
74 control,
75 dsl,
76 ),
77 store,
78 blob_store,
79 runtime_id,
80 #[cfg(test)]
81 force_input_snapshot_failure_for_test: false,
82 }
83 }
84
85 pub fn inner_ref(&self) -> &EphemeralRuntimeDriver {
87 &self.inner
88 }
89
90 pub(crate) fn rollback_snapshot(&self) -> EphemeralDriverRollbackSnapshot {
91 self.inner.rollback_snapshot()
92 }
93
94 pub(crate) fn restore_rollback_snapshot(&mut self, snapshot: EphemeralDriverRollbackSnapshot) {
95 self.inner.restore_rollback_snapshot(snapshot);
96 }
97
98 pub fn runtime_id(&self) -> &LogicalRuntimeId {
100 &self.runtime_id
101 }
102
103 pub(crate) async fn load_pending_compaction_projections(
104 &self,
105 ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeDriverError> {
106 self.store
107 .load_pending_compaction_projections(&self.runtime_id)
108 .await
109 .map_err(|error| {
110 RuntimeDriverError::Internal(format!(
111 "failed to load compaction projection outbox: {error}"
112 ))
113 })
114 }
115
116 pub(crate) async fn mark_compaction_projection_finalized(
117 &self,
118 projection: &meerkat_core::CompactionProjectionId,
119 ) -> Result<(), RuntimeDriverError> {
120 self.store
121 .mark_compaction_projection_finalized(&self.runtime_id, projection)
122 .await
123 .map_err(|error| {
124 RuntimeDriverError::Internal(format!(
125 "failed to finalize compaction projection outbox: {error}"
126 ))
127 })
128 }
129
130 pub(crate) async fn load_compaction_checkpoint_snapshot(
131 &self,
132 ) -> Result<Option<Vec<u8>>, RuntimeDriverError> {
133 self.store
134 .load_session_snapshot(&self.runtime_id)
135 .await
136 .map_err(|error| {
137 RuntimeDriverError::Internal(format!(
138 "failed to load authoritative compaction checkpoint snapshot: {error}"
139 ))
140 })
141 }
142
143 pub fn silent_comms_intents(&self) -> Vec<String> {
144 self.inner.silent_comms_intents()
145 }
146
147 pub fn is_idle(&self) -> bool {
149 self.inner.is_idle()
150 }
151
152 fn runtime_state_for_persistence(&self) -> Result<RuntimeState, RuntimeDriverError> {
154 Self::runtime_state_for_persistence_from_inner(&self.inner)
155 }
156
157 fn runtime_state_for_persistence_from_inner(
158 inner: &EphemeralRuntimeDriver,
159 ) -> Result<RuntimeState, RuntimeDriverError> {
160 crate::meerkat_machine::classify_runtime_lifecycle_durable_state(inner.runtime_state())
161 .map_err(|err| {
162 RuntimeDriverError::Internal(format!(
163 "generated runtime lifecycle durability classification failed: {err}"
164 ))
165 })
166 }
167
168 fn lifecycle_commit_for_persistence(
169 &self,
170 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
171 Self::lifecycle_commit_for_persistence_from_inner(&self.inner)
172 }
173
174 fn lifecycle_commit_for_persistence_with_supervisor_authority(
175 &self,
176 supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
177 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
178 Ok(
179 MachineLifecycleCommit::new_with_binding_and_unregister_progress(
180 Self::runtime_state_for_persistence_from_inner(&self.inner)?,
181 self.inner.machine_lifecycle_binding_facts(),
182 supervisor_authority,
183 Self::unregister_progress_for_persistence_from_inner(&self.inner),
184 ),
185 )
186 }
187
188 fn lifecycle_commit_for_persistence_from_inner(
189 inner: &EphemeralRuntimeDriver,
190 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
191 Ok(
192 MachineLifecycleCommit::new_with_binding_and_unregister_progress(
193 Self::runtime_state_for_persistence_from_inner(inner)?,
194 inner.machine_lifecycle_binding_facts(),
195 inner.supervisor_authority_snapshot(),
196 Self::unregister_progress_for_persistence_from_inner(inner),
197 ),
198 )
199 }
200
201 fn unregister_progress_for_persistence_from_inner(
202 inner: &EphemeralRuntimeDriver,
203 ) -> Option<crate::store::MachineUnregisterProgressSnapshot> {
204 let authority = inner.shared_dsl_authority();
205 let authority = authority
206 .lock()
207 .unwrap_or_else(std::sync::PoisonError::into_inner);
208 let state = authority.state();
209 (state.registration_phase == crate::meerkat_machine::dsl::RegistrationPhase::Draining).then(
210 || {
211 crate::store::MachineUnregisterProgressSnapshot::new(
212 state.unregister_runtime_loop_drain_pending,
213 state.unregister_comms_drain_exit_pending,
214 state.unregister_completion_waiter_drain_pending,
215 state.unregister_runtime_loop_forced_abort,
216 state.unregister_comms_drain_forced_abort,
217 )
218 },
219 )
220 }
221
222 fn lifecycle_persistence_payload_with_rollback(
232 &mut self,
233 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
234 context: &str,
235 ) -> Result<
236 (
237 super::ephemeral::EphemeralDriverRollbackSnapshot,
238 Vec<InputStatePersistenceRecord>,
239 MachineLifecycleCommit,
240 ),
241 RuntimeDriverError,
242 > {
243 let input_states_result = self.inner.authorized_stored_input_states_snapshot();
244 #[cfg(test)]
245 let input_states_result = if self.force_input_snapshot_failure_for_test {
246 Err(RuntimeDriverError::Internal(
247 "forced input-state snapshot failure for checkpoint-restore contract test"
248 .to_string(),
249 ))
250 } else {
251 input_states_result
252 };
253 let input_states = match input_states_result {
254 Ok(input_states) => input_states,
255 Err(err) => {
256 self.inner.restore_rollback_snapshot(checkpoint);
257 return Err(RuntimeDriverError::Internal(format!(
258 "{context} input-state snapshot failed: {err}"
259 )));
260 }
261 };
262 let commit = match self.lifecycle_commit_for_persistence() {
263 Ok(commit) => commit,
264 Err(err) => {
265 self.inner.restore_rollback_snapshot(checkpoint);
266 return Err(RuntimeDriverError::Internal(format!(
267 "{context} lifecycle commit classification failed: {err}"
268 )));
269 }
270 };
271 Ok((checkpoint, input_states, commit))
272 }
273
274 async fn commit_lifecycle_with_rollback(
275 &mut self,
276 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
277 target_state: RuntimeState,
278 context: &str,
279 ) -> Result<(), RuntimeDriverError> {
280 let (checkpoint, input_states, commit) =
285 self.lifecycle_persistence_payload_with_rollback(checkpoint, context)?;
286 let target_durable_state =
287 match crate::meerkat_machine::classify_runtime_lifecycle_durable_state(target_state) {
288 Ok(target_durable_state) => target_durable_state,
289 Err(err) => {
290 self.inner.restore_rollback_snapshot(checkpoint);
291 return Err(RuntimeDriverError::Internal(format!(
292 "{context} generated target lifecycle durability classification failed: {err}"
293 )));
294 }
295 };
296 if commit.runtime_state() != target_durable_state {
297 self.inner.restore_rollback_snapshot(checkpoint);
298 return Err(RuntimeDriverError::Internal(format!(
299 "{context} durable persist target {target_durable_state:?} from live {target_state:?} disagreed with generated lifecycle commit {:?}",
300 commit.runtime_state()
301 )));
302 }
303 if let Err(err) = self
304 .store
305 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
306 .await
307 {
308 self.inner.restore_rollback_snapshot(checkpoint);
309 return Err(RuntimeDriverError::Internal(format!(
310 "{context} persist failed: {err}"
311 )));
312 }
313 Ok(())
314 }
315
316 pub(crate) async fn publish_service_turn_terminal_lifecycle(
317 &mut self,
318 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
319 target_state: RuntimeState,
320 ) -> Result<(), RuntimeDriverError> {
321 self.commit_lifecycle_with_rollback(
322 checkpoint,
323 target_state,
324 "service turn terminal receipt",
325 )
326 .await?;
327 self.inner.sync_control_projection_from_dsl_authority();
328 Ok(())
329 }
330
331 pub(crate) fn set_control_projection(
332 &mut self,
333 next_phase: RuntimeState,
334 current_run_id: Option<RunId>,
335 pre_run_phase: Option<RuntimeState>,
336 ) {
337 self.inner
338 .set_control_projection(next_phase, current_run_id, pre_run_phase);
339 }
340
341 pub(crate) fn sync_control_projection_from_dsl_authority(&mut self) {
346 self.inner.sync_control_projection_from_dsl_authority();
347 }
348
349 pub(crate) async fn persist_current_machine_lifecycle(
350 &mut self,
351 context: &str,
352 ) -> Result<(), RuntimeDriverError> {
353 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
354 let commit = self.lifecycle_commit_for_persistence()?;
355 self.store
356 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
357 .await
358 .map_err(|err| {
359 RuntimeDriverError::Internal(format!("{context} lifecycle persist failed: {err}"))
360 })
361 }
362
363 pub(crate) async fn commit_unregister_finalization(
364 &mut self,
365 context: &str,
366 retired_ops_epoch: &meerkat_core::RuntimeEpochId,
367 ) -> Result<(), RuntimeDriverError> {
368 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
369 let commit = self
370 .lifecycle_commit_for_persistence()?
371 .for_unregister_finalization(retired_ops_epoch.clone());
372 self.store
373 .commit_unregister_finalization(&self.runtime_id, commit, &input_states)
374 .await
375 .map_err(|err| match err {
376 crate::store::RuntimeStoreError::UnregisterFinalizationOutcomeUnknown(reason) => {
377 RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
378 reason: format!("{context} lifecycle+ops finalization: {reason}"),
379 }
380 }
381 err => RuntimeDriverError::Internal(format!(
382 "{context} lifecycle+ops finalization failed: {err}"
383 )),
384 })
385 }
386
387 pub(crate) async fn persist_current_machine_lifecycle_with_supervisor_authority(
393 &mut self,
394 context: &str,
395 supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
396 ) -> Result<(), RuntimeDriverError> {
397 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
398 let commit =
399 self.lifecycle_commit_for_persistence_with_supervisor_authority(supervisor_authority)?;
400 self.store
401 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
402 .await
403 .map_err(|err| {
404 RuntimeDriverError::Internal(format!("{context} lifecycle persist failed: {err}"))
405 })
406 }
407
408 #[doc(hidden)]
411 pub fn contract_begin_run_authority(
412 &mut self,
413 run_id: RunId,
414 ) -> Result<(), RuntimeDriverError> {
415 self.inner.contract_begin_run_authority(run_id)
416 }
417
418 #[cfg(test)]
421 #[doc(hidden)]
422 pub(crate) fn contract_force_runtime_authority(
423 &mut self,
424 next_phase: RuntimeState,
425 current_run_id: Option<RunId>,
426 pre_run_phase: Option<RuntimeState>,
427 ) {
428 self.inner
429 .contract_force_runtime_authority(next_phase, current_run_id, pre_run_phase);
430 }
431
432 pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
434 self.inner.drain_events()
435 }
436
437 pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
439 self.inner.take_post_admission_signal()
440 }
441
442 pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
444 self.inner.post_admission_signal()
445 }
446
447 pub fn take_wake_requested(&mut self) -> bool {
449 self.inner.take_wake_requested()
450 }
451
452 pub fn take_process_requested(&mut self) -> bool {
454 self.inner.take_process_requested()
455 }
456
457 #[cfg(any(test, debug_assertions, feature = "test-support"))]
460 #[doc(hidden)]
461 pub fn contract_dequeue_next_for_recovery_tests(&mut self) -> Option<(InputId, Input)> {
462 self.inner.contract_dequeue_next_for_recovery_tests()
463 }
464
465 pub(crate) fn dequeue_batch_exact(
466 &mut self,
467 batch: &crate::meerkat_machine::driver::AuthorizedRuntimeLoopBatch,
468 ) -> Result<Vec<(InputId, Input)>, RuntimeDriverError> {
469 self.inner.dequeue_batch_exact(batch)
470 }
471
472 pub fn has_queued_input_outside(&self, excluded: &[InputId]) -> bool {
473 self.inner.has_queued_input_outside(excluded)
474 }
475
476 pub(crate) fn defer_queued_inputs_behind_backlog(
477 &mut self,
478 input_ids: &[InputId],
479 ) -> Result<(), RuntimeDriverError> {
480 self.inner.defer_queued_inputs_behind_backlog(input_ids)
481 }
482
483 pub(crate) fn absorb_post_admission_effects(
484 &mut self,
485 effects: &[crate::meerkat_machine::dsl::MeerkatMachineEffect],
486 ) {
487 self.inner.absorb_post_admission_effects(effects);
488 }
489
490 pub(crate) fn resolve_admission(
491 &self,
492 input: &Input,
493 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
494 self.inner.resolve_admission(input)
495 }
496
497 pub(crate) fn resolve_admission_with_active_turn_boundary(
498 &self,
499 input: &Input,
500 active_turn_boundary_available: bool,
501 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
502 self.inner
503 .resolve_admission_with_active_turn_boundary(input, active_turn_boundary_available)
504 }
505
506 pub(crate) fn resolve_admission_without_wake_with_active_turn_boundary(
507 &self,
508 input: &Input,
509 active_turn_boundary_available: bool,
510 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
511 self.inner
512 .resolve_admission_without_wake_with_active_turn_boundary(
513 input,
514 active_turn_boundary_available,
515 )
516 }
517
518 pub(crate) async fn accept_resolved_input(
519 &mut self,
520 input: Input,
521 resolved: crate::accept::ResolvedAdmission,
522 ) -> Result<AcceptOutcome, RuntimeDriverError> {
523 let mut staged = self.inner.clone_with_isolated_dsl_authority();
524 staged.ensure_contract_session_authority()?;
525 let staged_resolved = if resolved.authority().without_wake() {
526 staged.resolve_admission_without_wake_with_active_turn_boundary(
527 &input,
528 resolved.authority().active_turn_boundary_available(),
529 )?
530 } else {
531 staged.resolve_admission_with_active_turn_boundary(
532 &input,
533 resolved.authority().active_turn_boundary_available(),
534 )?
535 };
536 if !resolved.semantically_equivalent_to(&staged_resolved) {
537 return Err(RuntimeDriverError::Internal(format!(
538 "staged admission resolution diverged from preview: preview={resolved:?}, staged={staged_resolved:?}"
539 )));
540 }
541 let flags = staged_resolved.coarse_flags();
542 let staged_outcome = staged
543 .accept_resolved_input(input.clone(), staged_resolved)
544 .await?;
545
546 let AcceptOutcome::Accepted {
547 input_id: staged_input_id,
548 ..
549 } = staged_outcome
550 else {
551 return self.inner.accept_resolved_input(input, resolved).await;
552 };
553
554 staged.machine_apply_accept_with_completion_signal(&staged_input_id, flags)?;
555 let Some(mut staged_bundle) = staged.stored_input_state(&staged_input_id) else {
556 return Err(RuntimeDriverError::Internal(format!(
557 "generated input lifecycle phase missing for accepted input {staged_input_id}"
558 )));
559 };
560 let mut input_for_recovery = input.clone();
561 externalize_input_images(self.blob_store.as_ref(), &mut input_for_recovery)
562 .await
563 .map_err(|err| {
564 RuntimeDriverError::Internal(format!(
565 "failed to externalize runtime input images: {err}"
566 ))
567 })?;
568 staged_bundle.state.persisted_input = Some(input_for_recovery.clone());
569 self.persist_state(&staged_bundle).await?;
570
571 self.inner.ensure_contract_session_authority()?;
572 let mut outcome = self.inner.accept_resolved_input(input, resolved).await?;
573 if let AcceptOutcome::Accepted {
574 ref input_id,
575 ref mut state,
576 ref mut seed,
577 ..
578 } = outcome
579 {
580 if input_id != &staged_input_id {
581 return Err(RuntimeDriverError::Internal(format!(
582 "staged accepted input {staged_input_id} differed from committed input {input_id}"
583 )));
584 }
585 self.inner
586 .machine_apply_accept_with_completion_signal(input_id, flags)?;
587 let Some(mut bundle) = self.inner.stored_input_state(input_id) else {
588 return Err(RuntimeDriverError::Internal(format!(
589 "generated input lifecycle phase missing for accepted input {input_id}"
590 )));
591 };
592 bundle.state.persisted_input = Some(input_for_recovery);
593 self.inner.ledger_mut().accept(bundle.state.clone());
594 *state = bundle.state;
595 *seed = bundle.seed;
596 }
597
598 Ok(outcome)
599 }
600
601 pub(crate) async fn preview_accept_resolved_input(
602 &self,
603 input: Input,
604 resolved: &crate::accept::ResolvedAdmission,
605 ) -> Result<AcceptOutcome, RuntimeDriverError> {
606 let mut staged = self.inner.clone_with_isolated_dsl_authority();
607 staged.ensure_contract_session_authority()?;
608 let staged_resolved = if resolved.authority().without_wake() {
609 staged.resolve_admission_without_wake_with_active_turn_boundary(
610 &input,
611 resolved.authority().active_turn_boundary_available(),
612 )?
613 } else {
614 staged.resolve_admission_with_active_turn_boundary(
615 &input,
616 resolved.authority().active_turn_boundary_available(),
617 )?
618 };
619 if !resolved.semantically_equivalent_to(&staged_resolved) {
620 return Err(RuntimeDriverError::Internal(format!(
621 "staged admission preview diverged from caller resolution: preview={resolved:?}, staged={staged_resolved:?}"
622 )));
623 }
624 staged.accept_resolved_input(input, staged_resolved).await
625 }
626
627 pub(crate) fn machine_realize_authorized_stage_batch(
628 &mut self,
629 authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
630 ) -> Result<(), crate::traits::RuntimeDriverError> {
631 self.inner.machine_realize_authorized_stage_batch(authority)
632 }
633
634 pub fn apply_input(
636 &mut self,
637 input_id: &InputId,
638 run_id: &meerkat_core::lifecycle::RunId,
639 ) -> Result<(), crate::traits::RuntimeDriverError> {
640 self.inner.apply_input(input_id, run_id)
641 }
642
643 pub fn rollback_staged(
645 &mut self,
646 input_ids: &[InputId],
647 ) -> Result<(), crate::traits::RuntimeDriverError> {
648 self.inner.rollback_staged(input_ids)
649 }
650
651 async fn persist_state(&self, state: &StoredInputState) -> Result<(), RuntimeDriverError> {
652 let state = InputStatePersistenceRecord::from_machine_snapshot(state.clone())
653 .map_err(RuntimeDriverError::Internal)?;
654 self.store
655 .persist_input_state(&self.runtime_id, &state)
656 .await
657 .map_err(|e| RuntimeDriverError::Internal(e.to_string()))
658 }
659
660 pub(crate) async fn abandon_pending_inputs(
661 &mut self,
662 reason: InputAbandonReason,
663 ) -> Result<usize, RuntimeDriverError> {
664 let checkpoint = self.inner.rollback_snapshot();
665 let abandoned = match self.inner.abandon_pending_inputs(reason) {
666 Ok(abandoned) => abandoned,
667 Err(err) => {
668 self.inner.restore_rollback_snapshot(checkpoint);
669 return Err(err);
670 }
671 };
672 let (checkpoint, input_states, commit) =
673 self.lifecycle_persistence_payload_with_rollback(checkpoint, "pending input abandon")?;
674 if let Err(err) = self
675 .store
676 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
677 .await
678 {
679 self.inner.restore_rollback_snapshot(checkpoint);
680 return Err(RuntimeDriverError::Internal(format!(
681 "pending input abandon persist failed: {err}"
682 )));
683 }
684 Ok(abandoned)
685 }
686
687 pub(crate) async fn recycle_preserving_work(&mut self) -> Result<usize, RuntimeDriverError> {
692 let checkpoint = self.inner.rollback_snapshot();
693 let transferred = match self.inner.recycle_preserving_work() {
694 Ok(transferred) => transferred,
695 Err(err) => {
696 self.inner.restore_rollback_snapshot(checkpoint);
697 return Err(err);
698 }
699 };
700 let (checkpoint, input_states, commit) =
701 self.lifecycle_persistence_payload_with_rollback(checkpoint, "recycle")?;
702 if let Err(err) = self
703 .store
704 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
705 .await
706 {
707 self.inner.restore_rollback_snapshot(checkpoint);
708 return Err(RuntimeDriverError::Internal(format!(
709 "recycle persist failed: {err}"
710 )));
711 }
712
713 self.inner.sync_control_projection_from_dsl_authority();
714 Ok(transferred)
715 }
716
717 pub(crate) async fn realize_retire_lifecycle(
718 &mut self,
719 ) -> Result<crate::traits::RetireReport, RuntimeDriverError> {
720 let checkpoint = self.inner.rollback_snapshot();
721 let report = self.inner.finalize_retire();
722 let target_state = match self.runtime_state_for_persistence() {
725 Ok(target_state) => target_state,
726 Err(err) => {
727 self.inner.restore_rollback_snapshot(checkpoint);
728 return Err(err);
729 }
730 };
731 self.commit_lifecycle_with_rollback(checkpoint, target_state, "retire")
732 .await?;
733 self.inner.sync_control_projection_from_dsl_authority();
734 Ok(report)
735 }
736
737 pub(crate) async fn realize_reset_lifecycle(
738 &mut self,
739 ) -> Result<crate::traits::ResetReport, RuntimeDriverError> {
740 let checkpoint = self.inner.rollback_snapshot();
741 let report = match self.inner.reset_cleanup() {
742 Ok(report) => report,
743 Err(err) => {
744 self.inner.restore_rollback_snapshot(checkpoint);
745 return Err(err);
746 }
747 };
748 let target_state = match self.runtime_state_for_persistence() {
751 Ok(target_state) => target_state,
752 Err(err) => {
753 self.inner.restore_rollback_snapshot(checkpoint);
754 return Err(err);
755 }
756 };
757 self.commit_lifecycle_with_rollback(checkpoint, target_state, "reset")
758 .await?;
759 self.inner.sync_control_projection_from_dsl_authority();
760 Ok(report)
761 }
762
763 pub(crate) fn prepare_destroy_lifecycle(
764 &mut self,
765 ) -> Result<(EphemeralDriverRollbackSnapshot, DestroyReport), RuntimeDriverError> {
766 let checkpoint = self.inner.rollback_snapshot();
767 let abandoned = match self.inner.destroy_cleanup() {
768 Ok(abandoned) => abandoned,
769 Err(err) => {
770 self.inner.restore_rollback_snapshot(checkpoint);
771 return Err(err);
772 }
773 };
774 Ok((
775 checkpoint,
776 DestroyReport {
777 inputs_abandoned: abandoned,
778 },
779 ))
780 }
781
782 pub(crate) async fn commit_prepared_destroy_lifecycle(
783 &mut self,
784 checkpoint: EphemeralDriverRollbackSnapshot,
785 ) -> Result<(), RuntimeDriverError> {
786 let target_state = match self.runtime_state_for_persistence() {
791 Ok(target_state) => target_state,
792 Err(err) => {
793 self.inner.restore_rollback_snapshot(checkpoint);
794 return Err(err);
795 }
796 };
797 self.commit_lifecycle_with_rollback(checkpoint, target_state, "destroy")
798 .await
799 }
800
801 pub(crate) fn rollback_prepared_destroy_lifecycle(
802 &mut self,
803 checkpoint: EphemeralDriverRollbackSnapshot,
804 ) {
805 self.inner.restore_rollback_snapshot(checkpoint);
806 }
807
808 pub(crate) async fn finalize_runtime_executor_exit(
809 &mut self,
810 ) -> Result<(), RuntimeDriverError> {
811 let checkpoint = self.inner.rollback_snapshot();
812 if let Err(err) = self.inner.apply_runtime_executor_exited_authority() {
813 self.inner.restore_rollback_snapshot(checkpoint);
814 return Err(err);
815 }
816 if let Err(err) = self.inner.stop_runtime_cleanup() {
817 self.inner.restore_rollback_snapshot(checkpoint);
818 return Err(err);
819 }
820 let target_state = match self.runtime_state_for_persistence() {
824 Ok(target_state) => target_state,
825 Err(err) => {
826 self.inner.restore_rollback_snapshot(checkpoint);
827 return Err(err);
828 }
829 };
830 self.commit_lifecycle_with_rollback(checkpoint, target_state, "stop")
831 .await?;
832 self.inner.sync_control_projection_from_dsl_authority();
833 Ok(())
834 }
835
836 pub(crate) fn machine_realize_boundary_applied_in_memory(
837 &mut self,
838 run_id: &RunId,
839 receipt: &RunBoundaryReceipt,
840 ) -> Result<(), RuntimeDriverError> {
841 self.inner.machine_realize_boundary_applied(run_id, receipt)
842 }
843
844 pub(crate) fn machine_realize_run_completed_in_memory(
845 &mut self,
846 run_id: &RunId,
847 consumed_input_ids: &[InputId],
848 ) -> Result<(), RuntimeDriverError> {
849 self.inner
850 .machine_realize_run_completed(run_id, consumed_input_ids)
851 }
852
853 pub(crate) async fn machine_realize_live_boundary_context_injected(
854 &mut self,
855 run_id: &RunId,
856 input_ids: &[InputId],
857 stage_authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
858 session_snapshot: Option<Vec<u8>>,
859 ) -> Result<(), RuntimeDriverError> {
860 let checkpoint = self.inner.rollback_snapshot();
861 let receipt = match self.inner.machine_realize_live_boundary_context_injected(
862 run_id,
863 input_ids,
864 stage_authority,
865 ) {
866 Ok(receipt) => receipt,
867 Err(err) => {
868 self.inner.restore_rollback_snapshot(checkpoint);
869 return Err(err);
870 }
871 };
872 let input_updates = match self.inner.authorized_stored_input_states_snapshot() {
873 Ok(input_updates) => input_updates,
874 Err(err) => {
875 self.inner.restore_rollback_snapshot(checkpoint);
876 return Err(err);
877 }
878 };
879 if let Err(err) = self
880 .store
881 .atomic_apply(
882 &self.runtime_id,
883 session_snapshot
884 .as_ref()
885 .map(|session_snapshot| crate::store::SessionDelta {
886 session_snapshot: session_snapshot.clone(),
887 }),
888 receipt.clone(),
889 input_updates,
890 session_snapshot
891 .as_deref()
892 .and_then(|snapshot| {
893 serde_json::from_slice::<meerkat_core::Session>(snapshot).ok()
894 })
895 .map(|session| session.id().clone()),
896 )
897 .await
898 {
899 self.inner.restore_rollback_snapshot(checkpoint);
900 return Err(RuntimeDriverError::Internal(format!(
901 "runtime live-boundary context commit failed: {err}"
902 )));
903 }
904 Ok(())
905 }
906
907 pub(crate) async fn machine_commit_completed_boundary_snapshot(
908 &mut self,
909 receipt: &RunBoundaryReceipt,
910 session_snapshot: Option<&Vec<u8>>,
911 ) -> Result<(), RuntimeDriverError> {
912 let input_updates = self.inner.authorized_stored_input_states_snapshot()?;
913 self.store
914 .atomic_apply(
915 &self.runtime_id,
916 session_snapshot.map(|session_snapshot| crate::store::SessionDelta {
917 session_snapshot: session_snapshot.clone(),
918 }),
919 receipt.clone(),
920 input_updates,
921 session_snapshot
922 .and_then(|snapshot| {
923 serde_json::from_slice::<meerkat_core::Session>(snapshot).ok()
924 })
925 .map(|session| session.id().clone()),
926 )
927 .await
928 .map_err(|e| {
929 RuntimeDriverError::Internal(format!(
930 "runtime completed-boundary commit failed: {e}"
931 ))
932 })
933 }
934
935 pub(crate) async fn machine_realize_run_failed(
936 &mut self,
937 run_id: &RunId,
938 contributing_input_ids: &[InputId],
939 replay_plan: &super::ephemeral::ReplayQueuedContributorsPlan,
940 terminal_error: &str,
941 runtime_apply_failure: Option<&meerkat_core::lifecycle::CoreApplyFailureCause>,
942 recoverable: bool,
943 ) -> Result<(), RuntimeDriverError> {
944 let checkpoint = self.inner.rollback_snapshot();
945 if let Err(err) =
946 self.inner
947 .machine_realize_run_failed(run_id, contributing_input_ids, replay_plan)
948 {
949 self.inner.restore_rollback_snapshot(checkpoint);
950 return Err(err);
951 }
952 let failure_cause = runtime_apply_failure.map(|failure| failure.kind);
953 tracing::debug!(
954 run_id = ?run_id,
955 recoverable,
956 error = terminal_error,
957 failure_cause = ?failure_cause,
958 "persistent driver realized machine-owned failed-run replay"
959 );
960 let (checkpoint, input_states, commit) = self
961 .lifecycle_persistence_payload_with_rollback(checkpoint, "failed-run terminal event")?;
962 if let Err(err) = self
963 .store
964 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
965 .await
966 {
967 self.inner.restore_rollback_snapshot(checkpoint);
968 return Err(RuntimeDriverError::Internal(format!(
969 "terminal event persist failed: {err}"
970 )));
971 }
972 Ok(())
973 }
974
975 pub(crate) async fn machine_realize_run_cancelled(
976 &mut self,
977 run_id: &RunId,
978 contributing_input_ids: &[InputId],
979 ) -> Result<(), RuntimeDriverError> {
980 let checkpoint = self.inner.rollback_snapshot();
981 if let Err(err) = self
982 .inner
983 .machine_realize_run_cancelled(run_id, contributing_input_ids)
984 {
985 self.inner.restore_rollback_snapshot(checkpoint);
986 return Err(err);
987 }
988 tracing::debug!(
989 run_id = ?run_id,
990 contributors = contributing_input_ids.len(),
991 "persistent driver realized machine-owned cancelled run"
992 );
993 let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
994 checkpoint,
995 "cancelled-run terminal event",
996 )?;
997 if let Err(err) = self
998 .store
999 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1000 .await
1001 {
1002 self.inner.restore_rollback_snapshot(checkpoint);
1003 return Err(RuntimeDriverError::Internal(format!(
1004 "terminal cancellation persist failed: {err}"
1005 )));
1006 }
1007 Ok(())
1008 }
1009}
1010
1011#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1012#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1013impl RuntimeDriver for PersistentRuntimeDriver {
1014 async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError> {
1015 let resolved = self.resolve_admission(&input)?;
1016 self.accept_resolved_input(input, resolved).await
1017 }
1018
1019 async fn on_runtime_event(
1020 &mut self,
1021 event: RuntimeEventEnvelope,
1022 ) -> Result<(), RuntimeDriverError> {
1023 self.inner.on_runtime_event(event).await
1024 }
1025
1026 async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError> {
1027 let mut staged = self.inner.clone_with_isolated_dsl_authority();
1028 let report = crate::meerkat_machine::machine_recover_persistent_driver(
1029 self.store.as_ref(),
1030 &self.runtime_id,
1031 &mut staged,
1032 )
1033 .await?;
1034
1035 let input_states = staged.authorized_stored_input_states_snapshot()?;
1036 let commit = Self::lifecycle_commit_for_persistence_from_inner(&staged)?;
1037 self.store
1038 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1039 .await
1040 .map_err(|err| {
1041 RuntimeDriverError::Internal(format!("recovery persist failed: {err}"))
1042 })?;
1043 let _ = crate::meerkat_machine::machine_recover_persistent_driver(
1044 self.store.as_ref(),
1045 &self.runtime_id,
1046 &mut self.inner,
1047 )
1048 .await?;
1049 Ok(report)
1050 }
1051
1052 fn runtime_state(&self) -> RuntimeState {
1053 self.inner.runtime_state()
1054 }
1055
1056 fn input_state(&self, input_id: &InputId) -> Option<&InputState> {
1057 self.inner.input_state(input_id)
1058 }
1059
1060 fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState> {
1061 self.inner.input_phase(input_id)
1062 }
1063
1064 fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId> {
1065 self.inner.input_last_run_id(input_id)
1066 }
1067
1068 fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64> {
1069 self.inner.input_last_boundary_sequence(input_id)
1070 }
1071
1072 fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState> {
1073 self.inner.stored_input_state(input_id)
1074 }
1075
1076 fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
1077 self.inner.stored_input_states_snapshot()
1078 }
1079
1080 fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId> {
1081 self.inner.input_id_for_idempotency_key(idempotency_key)
1082 }
1083
1084 fn active_input_ids(&self) -> Vec<InputId> {
1085 self.inner.active_input_ids()
1086 }
1087}
1088
1089#[cfg(test)]
1090#[allow(clippy::unwrap_used, clippy::expect_used)]
1091mod tests {
1092 use super::*;
1093 use chrono::Utc;
1094 use meerkat_core::lifecycle::InputId;
1095
1096 fn make_prompt(text: &str) -> Input {
1097 Input::Prompt(crate::input::PromptInput {
1098 injected_context: Vec::new(),
1099 header: crate::input::InputHeader {
1100 id: InputId::new(),
1101 timestamp: Utc::now(),
1102 source: crate::input::InputOrigin::Operator,
1103 durability: crate::input::InputDurability::Durable,
1104 visibility: crate::input::InputVisibility::default(),
1105 idempotency_key: None,
1106 supersession_key: None,
1107 correlation_id: None,
1108 },
1109 content: text.into(),
1110 typed_turn_appends: Vec::new(),
1111 turn_metadata: None,
1112 })
1113 }
1114
1115 #[tokio::test]
1121 async fn commit_lifecycle_snapshot_failure_restores_checkpoint() {
1122 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1123 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1124 let rid = LogicalRuntimeId::new("commit-lifecycle-rollback-contract");
1125 let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1126
1127 let checkpoint = driver.rollback_snapshot();
1129
1130 let input = make_prompt("staged work");
1133 let input_id = input.id().clone();
1134 let outcome = driver.accept_input(input).await.unwrap();
1135 assert!(outcome.is_accepted());
1136 assert!(driver.input_phase(&input_id).is_some());
1137
1138 driver.force_input_snapshot_failure_for_test = true;
1140 let target_state = driver.inner_ref().runtime_state();
1141 let result = driver
1142 .commit_lifecycle_with_rollback(checkpoint, target_state, "test destroy")
1143 .await;
1144
1145 assert!(result.is_err(), "forced snapshot failure must propagate");
1148 assert!(
1149 driver.input_phase(&input_id).is_none(),
1150 "staged driver state must be restored to the pre-stage checkpoint"
1151 );
1152 assert!(driver.active_input_ids().is_empty());
1153 }
1154
1155 #[tokio::test]
1161 async fn abandon_pending_inputs_snapshot_failure_restores_checkpoint() {
1162 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1163 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1164 let rid = LogicalRuntimeId::new("abandon-rollback-contract");
1165 let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1166
1167 let input = make_prompt("pending work");
1169 let input_id = input.id().clone();
1170 let outcome = driver.accept_input(input).await.unwrap();
1171 assert!(outcome.is_accepted());
1172 assert!(driver.input_phase(&input_id).is_some());
1173
1174 driver.force_input_snapshot_failure_for_test = true;
1177 let result = driver
1178 .abandon_pending_inputs(InputAbandonReason::Reset)
1179 .await;
1180
1181 assert!(result.is_err(), "forced snapshot failure must propagate");
1182 assert!(
1183 driver.input_phase(&input_id).is_some(),
1184 "staged abandon must be rolled back: the pending input must still be live"
1185 );
1186 }
1187}