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