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 fn silent_comms_intents(&self) -> Vec<String> {
104 self.inner.silent_comms_intents()
105 }
106
107 pub fn is_idle(&self) -> bool {
109 self.inner.is_idle()
110 }
111
112 fn runtime_state_for_persistence(&self) -> Result<RuntimeState, RuntimeDriverError> {
114 Self::runtime_state_for_persistence_from_inner(&self.inner)
115 }
116
117 fn runtime_state_for_persistence_from_inner(
118 inner: &EphemeralRuntimeDriver,
119 ) -> Result<RuntimeState, RuntimeDriverError> {
120 crate::meerkat_machine::classify_runtime_lifecycle_durable_state(inner.runtime_state())
121 .map_err(|err| {
122 RuntimeDriverError::Internal(format!(
123 "generated runtime lifecycle durability classification failed: {err}"
124 ))
125 })
126 }
127
128 fn lifecycle_commit_for_persistence(
129 &self,
130 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
131 Self::lifecycle_commit_for_persistence_from_inner(&self.inner)
132 }
133
134 fn lifecycle_commit_for_persistence_from_inner(
135 inner: &EphemeralRuntimeDriver,
136 ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
137 Ok(MachineLifecycleCommit::new_with_binding(
138 Self::runtime_state_for_persistence_from_inner(inner)?,
139 inner.machine_lifecycle_binding_facts(),
140 ))
141 }
142
143 fn lifecycle_persistence_payload_with_rollback(
153 &mut self,
154 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
155 context: &str,
156 ) -> Result<
157 (
158 super::ephemeral::EphemeralDriverRollbackSnapshot,
159 Vec<InputStatePersistenceRecord>,
160 MachineLifecycleCommit,
161 ),
162 RuntimeDriverError,
163 > {
164 let input_states_result = self.inner.authorized_stored_input_states_snapshot();
165 #[cfg(test)]
166 let input_states_result = if self.force_input_snapshot_failure_for_test {
167 Err(RuntimeDriverError::Internal(
168 "forced input-state snapshot failure for checkpoint-restore contract test"
169 .to_string(),
170 ))
171 } else {
172 input_states_result
173 };
174 let input_states = match input_states_result {
175 Ok(input_states) => input_states,
176 Err(err) => {
177 self.inner.restore_rollback_snapshot(checkpoint);
178 return Err(RuntimeDriverError::Internal(format!(
179 "{context} input-state snapshot failed: {err}"
180 )));
181 }
182 };
183 let commit = match self.lifecycle_commit_for_persistence() {
184 Ok(commit) => commit,
185 Err(err) => {
186 self.inner.restore_rollback_snapshot(checkpoint);
187 return Err(RuntimeDriverError::Internal(format!(
188 "{context} lifecycle commit classification failed: {err}"
189 )));
190 }
191 };
192 Ok((checkpoint, input_states, commit))
193 }
194
195 async fn commit_lifecycle_with_rollback(
196 &mut self,
197 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
198 target_state: RuntimeState,
199 context: &str,
200 ) -> Result<(), RuntimeDriverError> {
201 let (checkpoint, input_states, commit) =
206 self.lifecycle_persistence_payload_with_rollback(checkpoint, context)?;
207 let target_durable_state =
208 match crate::meerkat_machine::classify_runtime_lifecycle_durable_state(target_state) {
209 Ok(target_durable_state) => target_durable_state,
210 Err(err) => {
211 self.inner.restore_rollback_snapshot(checkpoint);
212 return Err(RuntimeDriverError::Internal(format!(
213 "{context} generated target lifecycle durability classification failed: {err}"
214 )));
215 }
216 };
217 if commit.runtime_state() != target_durable_state {
218 self.inner.restore_rollback_snapshot(checkpoint);
219 return Err(RuntimeDriverError::Internal(format!(
220 "{context} durable persist target {target_durable_state:?} from live {target_state:?} disagreed with generated lifecycle commit {:?}",
221 commit.runtime_state()
222 )));
223 }
224 if let Err(err) = self
225 .store
226 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
227 .await
228 {
229 self.inner.restore_rollback_snapshot(checkpoint);
230 return Err(RuntimeDriverError::Internal(format!(
231 "{context} persist failed: {err}"
232 )));
233 }
234 Ok(())
235 }
236
237 pub(crate) async fn publish_service_turn_terminal_lifecycle(
238 &mut self,
239 checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
240 target_state: RuntimeState,
241 ) -> Result<(), RuntimeDriverError> {
242 self.commit_lifecycle_with_rollback(
243 checkpoint,
244 target_state,
245 "service turn terminal receipt",
246 )
247 .await?;
248 self.inner.sync_control_projection_from_dsl_authority();
249 Ok(())
250 }
251
252 pub(crate) fn set_control_projection(
253 &mut self,
254 next_phase: RuntimeState,
255 current_run_id: Option<RunId>,
256 pre_run_phase: Option<RuntimeState>,
257 ) {
258 self.inner
259 .set_control_projection(next_phase, current_run_id, pre_run_phase);
260 }
261
262 pub(crate) fn sync_control_projection_from_dsl_authority(&mut self) {
267 self.inner.sync_control_projection_from_dsl_authority();
268 }
269
270 pub(crate) async fn persist_current_machine_lifecycle(
271 &mut self,
272 context: &str,
273 ) -> Result<(), RuntimeDriverError> {
274 let input_states = self.inner.authorized_stored_input_states_snapshot()?;
275 let commit = self.lifecycle_commit_for_persistence()?;
276 self.store
277 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
278 .await
279 .map_err(|err| {
280 RuntimeDriverError::Internal(format!("{context} lifecycle persist failed: {err}"))
281 })
282 }
283
284 #[doc(hidden)]
287 pub fn contract_begin_run_authority(
288 &mut self,
289 run_id: RunId,
290 ) -> Result<(), RuntimeDriverError> {
291 self.inner.contract_begin_run_authority(run_id)
292 }
293
294 #[cfg(test)]
297 #[doc(hidden)]
298 pub(crate) fn contract_force_runtime_authority(
299 &mut self,
300 next_phase: RuntimeState,
301 current_run_id: Option<RunId>,
302 pre_run_phase: Option<RuntimeState>,
303 ) {
304 self.inner
305 .contract_force_runtime_authority(next_phase, current_run_id, pre_run_phase);
306 }
307
308 pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
310 self.inner.drain_events()
311 }
312
313 pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
315 self.inner.take_post_admission_signal()
316 }
317
318 pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
320 self.inner.post_admission_signal()
321 }
322
323 pub fn take_wake_requested(&mut self) -> bool {
325 self.inner.take_wake_requested()
326 }
327
328 pub fn take_process_requested(&mut self) -> bool {
330 self.inner.take_process_requested()
331 }
332
333 #[cfg(any(test, debug_assertions, feature = "test-support"))]
336 #[doc(hidden)]
337 pub fn contract_dequeue_next_for_recovery_tests(&mut self) -> Option<(InputId, Input)> {
338 self.inner.contract_dequeue_next_for_recovery_tests()
339 }
340
341 pub(crate) fn dequeue_batch_exact(
342 &mut self,
343 batch: &crate::meerkat_machine::driver::AuthorizedRuntimeLoopBatch,
344 ) -> Result<Vec<(InputId, Input)>, RuntimeDriverError> {
345 self.inner.dequeue_batch_exact(batch)
346 }
347
348 pub fn has_queued_input_outside(&self, excluded: &[InputId]) -> bool {
349 self.inner.has_queued_input_outside(excluded)
350 }
351
352 pub(crate) fn defer_queued_inputs_behind_backlog(
353 &mut self,
354 input_ids: &[InputId],
355 ) -> Result<(), RuntimeDriverError> {
356 self.inner.defer_queued_inputs_behind_backlog(input_ids)
357 }
358
359 pub(crate) fn absorb_post_admission_effects(
360 &mut self,
361 effects: &[crate::meerkat_machine::dsl::MeerkatMachineEffect],
362 ) {
363 self.inner.absorb_post_admission_effects(effects);
364 }
365
366 pub(crate) fn resolve_admission(
367 &self,
368 input: &Input,
369 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
370 self.inner.resolve_admission(input)
371 }
372
373 pub(crate) fn resolve_admission_with_active_turn_boundary(
374 &self,
375 input: &Input,
376 active_turn_boundary_available: bool,
377 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
378 self.inner
379 .resolve_admission_with_active_turn_boundary(input, active_turn_boundary_available)
380 }
381
382 pub(crate) fn resolve_admission_without_wake_with_active_turn_boundary(
383 &self,
384 input: &Input,
385 active_turn_boundary_available: bool,
386 ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
387 self.inner
388 .resolve_admission_without_wake_with_active_turn_boundary(
389 input,
390 active_turn_boundary_available,
391 )
392 }
393
394 pub(crate) async fn accept_resolved_input(
395 &mut self,
396 input: Input,
397 resolved: crate::accept::ResolvedAdmission,
398 ) -> Result<AcceptOutcome, RuntimeDriverError> {
399 let mut staged = self.inner.clone_with_isolated_dsl_authority();
400 staged.ensure_contract_session_authority()?;
401 let staged_resolved = if resolved.authority().without_wake() {
402 staged.resolve_admission_without_wake_with_active_turn_boundary(
403 &input,
404 resolved.authority().active_turn_boundary_available(),
405 )?
406 } else {
407 staged.resolve_admission_with_active_turn_boundary(
408 &input,
409 resolved.authority().active_turn_boundary_available(),
410 )?
411 };
412 if !resolved.semantically_equivalent_to(&staged_resolved) {
413 return Err(RuntimeDriverError::Internal(format!(
414 "staged admission resolution diverged from preview: preview={resolved:?}, staged={staged_resolved:?}"
415 )));
416 }
417 let flags = staged_resolved.coarse_flags();
418 let staged_outcome = staged
419 .accept_resolved_input(input.clone(), staged_resolved)
420 .await?;
421
422 let AcceptOutcome::Accepted {
423 input_id: staged_input_id,
424 ..
425 } = staged_outcome
426 else {
427 return self.inner.accept_resolved_input(input, resolved).await;
428 };
429
430 staged.machine_apply_accept_with_completion_signal(&staged_input_id, flags)?;
431 let Some(mut staged_bundle) = staged.stored_input_state(&staged_input_id) else {
432 return Err(RuntimeDriverError::Internal(format!(
433 "generated input lifecycle phase missing for accepted input {staged_input_id}"
434 )));
435 };
436 let mut input_for_recovery = input.clone();
437 externalize_input_images(self.blob_store.as_ref(), &mut input_for_recovery)
438 .await
439 .map_err(|err| {
440 RuntimeDriverError::Internal(format!(
441 "failed to externalize runtime input images: {err}"
442 ))
443 })?;
444 staged_bundle.state.persisted_input = Some(input_for_recovery.clone());
445 self.persist_state(&staged_bundle).await?;
446
447 self.inner.ensure_contract_session_authority()?;
448 let mut outcome = self.inner.accept_resolved_input(input, resolved).await?;
449 if let AcceptOutcome::Accepted {
450 ref input_id,
451 ref mut state,
452 ref mut seed,
453 ..
454 } = outcome
455 {
456 if input_id != &staged_input_id {
457 return Err(RuntimeDriverError::Internal(format!(
458 "staged accepted input {staged_input_id} differed from committed input {input_id}"
459 )));
460 }
461 self.inner
462 .machine_apply_accept_with_completion_signal(input_id, flags)?;
463 let Some(mut bundle) = self.inner.stored_input_state(input_id) else {
464 return Err(RuntimeDriverError::Internal(format!(
465 "generated input lifecycle phase missing for accepted input {input_id}"
466 )));
467 };
468 bundle.state.persisted_input = Some(input_for_recovery);
469 self.inner.ledger_mut().accept(bundle.state.clone());
470 *state = bundle.state;
471 *seed = bundle.seed;
472 }
473
474 Ok(outcome)
475 }
476
477 pub(crate) async fn preview_accept_resolved_input(
478 &self,
479 input: Input,
480 resolved: &crate::accept::ResolvedAdmission,
481 ) -> Result<AcceptOutcome, RuntimeDriverError> {
482 let mut staged = self.inner.clone_with_isolated_dsl_authority();
483 staged.ensure_contract_session_authority()?;
484 let staged_resolved = if resolved.authority().without_wake() {
485 staged.resolve_admission_without_wake_with_active_turn_boundary(
486 &input,
487 resolved.authority().active_turn_boundary_available(),
488 )?
489 } else {
490 staged.resolve_admission_with_active_turn_boundary(
491 &input,
492 resolved.authority().active_turn_boundary_available(),
493 )?
494 };
495 if !resolved.semantically_equivalent_to(&staged_resolved) {
496 return Err(RuntimeDriverError::Internal(format!(
497 "staged admission preview diverged from caller resolution: preview={resolved:?}, staged={staged_resolved:?}"
498 )));
499 }
500 staged.accept_resolved_input(input, staged_resolved).await
501 }
502
503 pub(crate) fn machine_realize_authorized_stage_batch(
504 &mut self,
505 authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
506 ) -> Result<(), crate::traits::RuntimeDriverError> {
507 self.inner.machine_realize_authorized_stage_batch(authority)
508 }
509
510 pub fn apply_input(
512 &mut self,
513 input_id: &InputId,
514 run_id: &meerkat_core::lifecycle::RunId,
515 ) -> Result<(), crate::traits::RuntimeDriverError> {
516 self.inner.apply_input(input_id, run_id)
517 }
518
519 pub fn rollback_staged(
521 &mut self,
522 input_ids: &[InputId],
523 ) -> Result<(), crate::traits::RuntimeDriverError> {
524 self.inner.rollback_staged(input_ids)
525 }
526
527 async fn persist_state(&self, state: &StoredInputState) -> Result<(), RuntimeDriverError> {
528 let state = InputStatePersistenceRecord::from_machine_snapshot(state.clone())
529 .map_err(RuntimeDriverError::Internal)?;
530 self.store
531 .persist_input_state(&self.runtime_id, &state)
532 .await
533 .map_err(|e| RuntimeDriverError::Internal(e.to_string()))
534 }
535
536 pub(crate) async fn abandon_pending_inputs(
537 &mut self,
538 reason: InputAbandonReason,
539 ) -> Result<usize, RuntimeDriverError> {
540 let checkpoint = self.inner.rollback_snapshot();
541 let abandoned = match self.inner.abandon_pending_inputs(reason) {
542 Ok(abandoned) => abandoned,
543 Err(err) => {
544 self.inner.restore_rollback_snapshot(checkpoint);
545 return Err(err);
546 }
547 };
548 let (checkpoint, input_states, commit) =
549 self.lifecycle_persistence_payload_with_rollback(checkpoint, "pending input abandon")?;
550 if let Err(err) = self
551 .store
552 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
553 .await
554 {
555 self.inner.restore_rollback_snapshot(checkpoint);
556 return Err(RuntimeDriverError::Internal(format!(
557 "pending input abandon persist failed: {err}"
558 )));
559 }
560 Ok(abandoned)
561 }
562
563 pub(crate) async fn recycle_preserving_work(&mut self) -> Result<usize, RuntimeDriverError> {
568 let checkpoint = self.inner.rollback_snapshot();
569 let transferred = match self.inner.recycle_preserving_work() {
570 Ok(transferred) => transferred,
571 Err(err) => {
572 self.inner.restore_rollback_snapshot(checkpoint);
573 return Err(err);
574 }
575 };
576 let (checkpoint, input_states, commit) =
577 self.lifecycle_persistence_payload_with_rollback(checkpoint, "recycle")?;
578 if let Err(err) = self
579 .store
580 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
581 .await
582 {
583 self.inner.restore_rollback_snapshot(checkpoint);
584 return Err(RuntimeDriverError::Internal(format!(
585 "recycle persist failed: {err}"
586 )));
587 }
588
589 self.inner.sync_control_projection_from_dsl_authority();
590 Ok(transferred)
591 }
592
593 pub(crate) async fn realize_retire_lifecycle(
594 &mut self,
595 ) -> Result<crate::traits::RetireReport, RuntimeDriverError> {
596 let checkpoint = self.inner.rollback_snapshot();
597 let report = self.inner.finalize_retire();
598 let target_state = match self.runtime_state_for_persistence() {
601 Ok(target_state) => target_state,
602 Err(err) => {
603 self.inner.restore_rollback_snapshot(checkpoint);
604 return Err(err);
605 }
606 };
607 self.commit_lifecycle_with_rollback(checkpoint, target_state, "retire")
608 .await?;
609 self.inner.sync_control_projection_from_dsl_authority();
610 Ok(report)
611 }
612
613 pub(crate) async fn realize_reset_lifecycle(
614 &mut self,
615 ) -> Result<crate::traits::ResetReport, RuntimeDriverError> {
616 let checkpoint = self.inner.rollback_snapshot();
617 let report = match self.inner.reset_cleanup() {
618 Ok(report) => report,
619 Err(err) => {
620 self.inner.restore_rollback_snapshot(checkpoint);
621 return Err(err);
622 }
623 };
624 let target_state = match self.runtime_state_for_persistence() {
627 Ok(target_state) => target_state,
628 Err(err) => {
629 self.inner.restore_rollback_snapshot(checkpoint);
630 return Err(err);
631 }
632 };
633 self.commit_lifecycle_with_rollback(checkpoint, target_state, "reset")
634 .await?;
635 self.inner.sync_control_projection_from_dsl_authority();
636 Ok(report)
637 }
638
639 pub(crate) fn prepare_destroy_lifecycle(
640 &mut self,
641 ) -> Result<(EphemeralDriverRollbackSnapshot, DestroyReport), RuntimeDriverError> {
642 let checkpoint = self.inner.rollback_snapshot();
643 let abandoned = match self.inner.destroy_cleanup() {
644 Ok(abandoned) => abandoned,
645 Err(err) => {
646 self.inner.restore_rollback_snapshot(checkpoint);
647 return Err(err);
648 }
649 };
650 Ok((
651 checkpoint,
652 DestroyReport {
653 inputs_abandoned: abandoned,
654 },
655 ))
656 }
657
658 pub(crate) async fn commit_prepared_destroy_lifecycle(
659 &mut self,
660 checkpoint: EphemeralDriverRollbackSnapshot,
661 ) -> Result<(), RuntimeDriverError> {
662 let target_state = match self.runtime_state_for_persistence() {
667 Ok(target_state) => target_state,
668 Err(err) => {
669 self.inner.restore_rollback_snapshot(checkpoint);
670 return Err(err);
671 }
672 };
673 self.commit_lifecycle_with_rollback(checkpoint, target_state, "destroy")
674 .await
675 }
676
677 pub(crate) fn rollback_prepared_destroy_lifecycle(
678 &mut self,
679 checkpoint: EphemeralDriverRollbackSnapshot,
680 ) {
681 self.inner.restore_rollback_snapshot(checkpoint);
682 }
683
684 pub(crate) async fn finalize_runtime_executor_exit(
685 &mut self,
686 ) -> Result<(), RuntimeDriverError> {
687 let checkpoint = self.inner.rollback_snapshot();
688 if let Err(err) = self.inner.apply_runtime_executor_exited_authority() {
689 self.inner.restore_rollback_snapshot(checkpoint);
690 return Err(err);
691 }
692 if let Err(err) = self.inner.stop_runtime_cleanup() {
693 self.inner.restore_rollback_snapshot(checkpoint);
694 return Err(err);
695 }
696 let target_state = match self.runtime_state_for_persistence() {
700 Ok(target_state) => target_state,
701 Err(err) => {
702 self.inner.restore_rollback_snapshot(checkpoint);
703 return Err(err);
704 }
705 };
706 self.commit_lifecycle_with_rollback(checkpoint, target_state, "stop")
707 .await?;
708 self.inner.sync_control_projection_from_dsl_authority();
709 Ok(())
710 }
711
712 pub(crate) fn machine_realize_boundary_applied_in_memory(
713 &mut self,
714 run_id: &RunId,
715 receipt: &RunBoundaryReceipt,
716 ) -> Result<(), RuntimeDriverError> {
717 self.inner.machine_realize_boundary_applied(run_id, receipt)
718 }
719
720 pub(crate) fn machine_realize_run_completed_in_memory(
721 &mut self,
722 run_id: &RunId,
723 consumed_input_ids: &[InputId],
724 ) -> Result<(), RuntimeDriverError> {
725 self.inner
726 .machine_realize_run_completed(run_id, consumed_input_ids)
727 }
728
729 pub(crate) async fn machine_realize_live_boundary_context_injected(
730 &mut self,
731 run_id: &RunId,
732 input_ids: &[InputId],
733 stage_authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
734 session_snapshot: Option<Vec<u8>>,
735 ) -> Result<(), RuntimeDriverError> {
736 let checkpoint = self.inner.rollback_snapshot();
737 let receipt = match self.inner.machine_realize_live_boundary_context_injected(
738 run_id,
739 input_ids,
740 stage_authority,
741 ) {
742 Ok(receipt) => receipt,
743 Err(err) => {
744 self.inner.restore_rollback_snapshot(checkpoint);
745 return Err(err);
746 }
747 };
748 let input_updates = match self.inner.authorized_stored_input_states_snapshot() {
749 Ok(input_updates) => input_updates,
750 Err(err) => {
751 self.inner.restore_rollback_snapshot(checkpoint);
752 return Err(err);
753 }
754 };
755 if let Err(err) = self
756 .store
757 .atomic_apply(
758 &self.runtime_id,
759 session_snapshot
760 .as_ref()
761 .map(|session_snapshot| crate::store::SessionDelta {
762 session_snapshot: session_snapshot.clone(),
763 }),
764 receipt.clone(),
765 input_updates,
766 session_snapshot
767 .as_deref()
768 .and_then(|snapshot| {
769 serde_json::from_slice::<meerkat_core::Session>(snapshot).ok()
770 })
771 .map(|session| session.id().clone()),
772 )
773 .await
774 {
775 self.inner.restore_rollback_snapshot(checkpoint);
776 return Err(RuntimeDriverError::Internal(format!(
777 "runtime live-boundary context commit failed: {err}"
778 )));
779 }
780 Ok(())
781 }
782
783 pub(crate) async fn machine_commit_completed_boundary_snapshot(
784 &mut self,
785 receipt: &RunBoundaryReceipt,
786 session_snapshot: Option<&Vec<u8>>,
787 ) -> Result<(), RuntimeDriverError> {
788 let input_updates = self.inner.authorized_stored_input_states_snapshot()?;
789 self.store
790 .atomic_apply(
791 &self.runtime_id,
792 session_snapshot.map(|session_snapshot| crate::store::SessionDelta {
793 session_snapshot: session_snapshot.clone(),
794 }),
795 receipt.clone(),
796 input_updates,
797 session_snapshot
798 .and_then(|snapshot| {
799 serde_json::from_slice::<meerkat_core::Session>(snapshot).ok()
800 })
801 .map(|session| session.id().clone()),
802 )
803 .await
804 .map_err(|e| {
805 RuntimeDriverError::Internal(format!(
806 "runtime completed-boundary commit failed: {e}"
807 ))
808 })
809 }
810
811 pub(crate) async fn machine_realize_run_failed(
812 &mut self,
813 run_id: &RunId,
814 contributing_input_ids: &[InputId],
815 replay_plan: &super::ephemeral::ReplayQueuedContributorsPlan,
816 terminal_error: &str,
817 runtime_apply_failure: Option<&meerkat_core::lifecycle::CoreApplyFailureCause>,
818 recoverable: bool,
819 ) -> Result<(), RuntimeDriverError> {
820 let checkpoint = self.inner.rollback_snapshot();
821 if let Err(err) =
822 self.inner
823 .machine_realize_run_failed(run_id, contributing_input_ids, replay_plan)
824 {
825 self.inner.restore_rollback_snapshot(checkpoint);
826 return Err(err);
827 }
828 let failure_cause = runtime_apply_failure.map(|failure| failure.kind);
829 tracing::debug!(
830 run_id = ?run_id,
831 recoverable,
832 error = terminal_error,
833 failure_cause = ?failure_cause,
834 "persistent driver realized machine-owned failed-run replay"
835 );
836 let (checkpoint, input_states, commit) = self
837 .lifecycle_persistence_payload_with_rollback(checkpoint, "failed-run terminal event")?;
838 if let Err(err) = self
839 .store
840 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
841 .await
842 {
843 self.inner.restore_rollback_snapshot(checkpoint);
844 return Err(RuntimeDriverError::Internal(format!(
845 "terminal event persist failed: {err}"
846 )));
847 }
848 Ok(())
849 }
850
851 pub(crate) async fn machine_realize_run_cancelled(
852 &mut self,
853 run_id: &RunId,
854 contributing_input_ids: &[InputId],
855 ) -> Result<(), RuntimeDriverError> {
856 let checkpoint = self.inner.rollback_snapshot();
857 if let Err(err) = self
858 .inner
859 .machine_realize_run_cancelled(run_id, contributing_input_ids)
860 {
861 self.inner.restore_rollback_snapshot(checkpoint);
862 return Err(err);
863 }
864 tracing::debug!(
865 run_id = ?run_id,
866 contributors = contributing_input_ids.len(),
867 "persistent driver realized machine-owned cancelled run"
868 );
869 let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
870 checkpoint,
871 "cancelled-run terminal event",
872 )?;
873 if let Err(err) = self
874 .store
875 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
876 .await
877 {
878 self.inner.restore_rollback_snapshot(checkpoint);
879 return Err(RuntimeDriverError::Internal(format!(
880 "terminal cancellation persist failed: {err}"
881 )));
882 }
883 Ok(())
884 }
885}
886
887#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
888#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
889impl RuntimeDriver for PersistentRuntimeDriver {
890 async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError> {
891 let resolved = self.resolve_admission(&input)?;
892 self.accept_resolved_input(input, resolved).await
893 }
894
895 async fn on_runtime_event(
896 &mut self,
897 event: RuntimeEventEnvelope,
898 ) -> Result<(), RuntimeDriverError> {
899 self.inner.on_runtime_event(event).await
900 }
901
902 async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError> {
903 let mut staged = self.inner.clone_with_isolated_dsl_authority();
904 let report = crate::meerkat_machine::machine_recover_persistent_driver(
905 self.store.as_ref(),
906 &self.runtime_id,
907 &mut staged,
908 )
909 .await?;
910
911 let input_states = staged.authorized_stored_input_states_snapshot()?;
912 let commit = Self::lifecycle_commit_for_persistence_from_inner(&staged)?;
913 self.store
914 .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
915 .await
916 .map_err(|err| {
917 RuntimeDriverError::Internal(format!("recovery persist failed: {err}"))
918 })?;
919 let _ = crate::meerkat_machine::machine_recover_persistent_driver(
920 self.store.as_ref(),
921 &self.runtime_id,
922 &mut self.inner,
923 )
924 .await?;
925 Ok(report)
926 }
927
928 fn runtime_state(&self) -> RuntimeState {
929 self.inner.runtime_state()
930 }
931
932 fn input_state(&self, input_id: &InputId) -> Option<&InputState> {
933 self.inner.input_state(input_id)
934 }
935
936 fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState> {
937 self.inner.input_phase(input_id)
938 }
939
940 fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId> {
941 self.inner.input_last_run_id(input_id)
942 }
943
944 fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64> {
945 self.inner.input_last_boundary_sequence(input_id)
946 }
947
948 fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState> {
949 self.inner.stored_input_state(input_id)
950 }
951
952 fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
953 self.inner.stored_input_states_snapshot()
954 }
955
956 fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId> {
957 self.inner.input_id_for_idempotency_key(idempotency_key)
958 }
959
960 fn active_input_ids(&self) -> Vec<InputId> {
961 self.inner.active_input_ids()
962 }
963}
964
965#[cfg(test)]
966#[allow(clippy::unwrap_used, clippy::expect_used)]
967mod tests {
968 use super::*;
969 use chrono::Utc;
970 use meerkat_core::lifecycle::InputId;
971
972 fn make_prompt(text: &str) -> Input {
973 Input::Prompt(crate::input::PromptInput {
974 injected_context: Vec::new(),
975 header: crate::input::InputHeader {
976 id: InputId::new(),
977 timestamp: Utc::now(),
978 source: crate::input::InputOrigin::Operator,
979 durability: crate::input::InputDurability::Durable,
980 visibility: crate::input::InputVisibility::default(),
981 idempotency_key: None,
982 supersession_key: None,
983 correlation_id: None,
984 },
985 content: text.into(),
986 typed_turn_appends: Vec::new(),
987 turn_metadata: None,
988 })
989 }
990
991 #[tokio::test]
997 async fn commit_lifecycle_snapshot_failure_restores_checkpoint() {
998 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
999 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1000 let rid = LogicalRuntimeId::new("commit-lifecycle-rollback-contract");
1001 let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1002
1003 let checkpoint = driver.rollback_snapshot();
1005
1006 let input = make_prompt("staged work");
1009 let input_id = input.id().clone();
1010 let outcome = driver.accept_input(input).await.unwrap();
1011 assert!(outcome.is_accepted());
1012 assert!(driver.input_phase(&input_id).is_some());
1013
1014 driver.force_input_snapshot_failure_for_test = true;
1016 let target_state = driver.inner_ref().runtime_state();
1017 let result = driver
1018 .commit_lifecycle_with_rollback(checkpoint, target_state, "test destroy")
1019 .await;
1020
1021 assert!(result.is_err(), "forced snapshot failure must propagate");
1024 assert!(
1025 driver.input_phase(&input_id).is_none(),
1026 "staged driver state must be restored to the pre-stage checkpoint"
1027 );
1028 assert!(driver.active_input_ids().is_empty());
1029 }
1030
1031 #[tokio::test]
1037 async fn abandon_pending_inputs_snapshot_failure_restores_checkpoint() {
1038 let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1039 let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1040 let rid = LogicalRuntimeId::new("abandon-rollback-contract");
1041 let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1042
1043 let input = make_prompt("pending work");
1045 let input_id = input.id().clone();
1046 let outcome = driver.accept_input(input).await.unwrap();
1047 assert!(outcome.is_accepted());
1048 assert!(driver.input_phase(&input_id).is_some());
1049
1050 driver.force_input_snapshot_failure_for_test = true;
1053 let result = driver
1054 .abandon_pending_inputs(InputAbandonReason::Reset)
1055 .await;
1056
1057 assert!(result.is_err(), "forced snapshot failure must propagate");
1058 assert!(
1059 driver.input_phase(&input_id).is_some(),
1060 "staged abandon must be rolled back: the pending input must still be live"
1061 );
1062 }
1063}