Skip to main content

meerkat_runtime/driver/
persistent.rs

1//! PersistentRuntimeDriver — wraps EphemeralRuntimeDriver + RuntimeStore.
2//!
3//! Provides durable-before-ack guarantee: InputState is persisted via
4//! RuntimeStore BEFORE returning AcceptOutcome. Delegates state machine
5//! logic to the ephemeral driver.
6
7use 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
29/// Persistent runtime driver — durable InputState via RuntimeStore.
30pub struct PersistentRuntimeDriver {
31    /// Underlying ephemeral driver for state machine logic.
32    inner: EphemeralRuntimeDriver,
33    /// Durable store for InputState + receipts.
34    store: Arc<dyn RuntimeStore>,
35    /// Blob store used to externalize durable input payloads.
36    blob_store: Arc<dyn BlobStore>,
37    /// Runtime ID for store operations.
38    runtime_id: LogicalRuntimeId,
39    /// Test-only fault injection: forces the input-state snapshot step of
40    /// [`Self::commit_lifecycle_with_rollback`] to fail so tests can pin the
41    /// checkpoint-restore contract for that arm.
42    #[cfg(test)]
43    pub(crate) force_input_snapshot_failure_for_test: bool,
44}
45
46impl PersistentRuntimeDriver {
47    /// Create a new persistent runtime driver.
48    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    /// Get immutable reference to the inner ephemeral driver.
86    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    /// Get the logical runtime ID for this driver.
99    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    /// Check if the runtime is idle (delegates to inner).
108    pub fn is_idle(&self) -> bool {
109        self.inner.is_idle()
110    }
111
112    /// Ask generated MeerkatMachine authority for the store-visible lifecycle.
113    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    /// Snapshot + classify the lifecycle persistence payload, restoring the
156    /// caller's checkpoint on failure.
157    ///
158    /// Contract (Dogma K11): every fallible step between a staged `&mut` DSL
159    /// transition and the rollback-guarded durable commit restores the
160    /// caller's checkpoint. A bare `?` here would leave the staged lifecycle
161    /// live in driver state while reporting failure to the caller. The
162    /// checkpoint is returned on success so the durable commit arm can keep
163    /// using it.
164    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        // Contract: every fallible step between the staged DSL transition and
214        // the durable commit restores the caller's checkpoint on failure. A
215        // bare `?` here would leave the staged lifecycle (e.g. Destroy) live
216        // in driver state while reporting failure to the caller.
217        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    /// Low-level control projection shim for external contract tests.
275    ///
276    /// This does not decide lifecycle legality; it only applies an already
277    /// chosen MeerkatMachine control projection to the concrete driver shell.
278    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    /// Persist a previewed closed supervisor projection alongside the current
297    /// machine lifecycle. This lets the supervisor saga commit durable truth
298    /// before changing the shared live authority, avoiding a whole-authority
299    /// rollback across asynchronous store I/O (peer ingress may concurrently
300    /// mutate unrelated generated fields).
301    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    /// Contract helper for external tests that need to start a run through the
318    /// same DSL authority used by the runtime loop.
319    #[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    /// Test-only authority override for crate-unit tests that need to seed
328    /// impossible or already-realized runtime phases.
329    #[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    /// Get pending events (delegates to inner).
342    pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
343        self.inner.drain_events()
344    }
345
346    /// Drain the typed post-admission signal (delegates to inner).
347    pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
348        self.inner.take_post_admission_signal()
349    }
350
351    /// Inspect the current typed post-admission signal without draining it.
352    pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
353        self.inner.post_admission_signal()
354    }
355
356    /// Check and clear wake flag (backward-compat, delegates to inner).
357    pub fn take_wake_requested(&mut self) -> bool {
358        self.inner.take_wake_requested()
359    }
360
361    /// Check and clear immediate processing flag (backward-compat, delegates to inner).
362    pub fn take_process_requested(&mut self) -> bool {
363        self.inner.take_process_requested()
364    }
365
366    /// Contract helper for recovery/queue-projection tests. Production runtime
367    /// execution must use generated batch authority via `dequeue_batch_exact`.
368    #[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    /// Apply input (delegates to inner).
544    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    /// Roll back staged inputs (delegates to inner).
553    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    /// Recycle the in-memory driver shell while preserving canonical pending
597    /// work from durable runtime truth.
598    ///
599    /// Unlike `reset()`, this must not abandon queued/staged work.
600    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        // Restore the checkpoint on classification failure: an early `?` here
632        // would leave the finalized retire state live without rollback.
633        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        // Restore the checkpoint on classification failure: an early `?` here
658        // would leave the reset-cleaned state live without rollback.
659        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        // Resolve the durable target BEFORE handing the checkpoint to the
696        // commit helper: an early `?` here would otherwise leave the staged
697        // destroy state live without restoring the checkpoint (driver-side
698        // shadow truth with no rollback).
699        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        // Resolve the durable target BEFORE handing the checkpoint to the
730        // commit helper, so a classification failure restores the staged
731        // executor-exit state instead of leaving it live without rollback.
732        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    /// Dogma K11 (Persistent destroy / driver-side shadow truth): every
1025    /// fallible step of `commit_lifecycle_with_rollback` AFTER the caller has
1026    /// staged a DSL lifecycle transition must restore the caller's checkpoint.
1027    /// The input-state snapshot read used to escape with a bare `?`, leaving
1028    /// the staged lifecycle live in driver state while reporting failure.
1029    #[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        // Checkpoint BEFORE any state mutation (the caller's pre-stage view).
1037        let checkpoint = driver.rollback_snapshot();
1038
1039        // Mutate driver state past the checkpoint (stands in for a staged
1040        // Destroy/lifecycle transition awaiting durable commit).
1041        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        // Inject a failure into the input-state snapshot step.
1048        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        // The failure must propagate typed AND the staged driver state must be
1055        // rolled back to the checkpoint — no half-destroyed shadow truth.
1056        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    /// Same K11 checkpoint-restore contract for `abandon_pending_inputs`: the
1065    /// input-state snapshot / lifecycle-commit classification steps between
1066    /// the staged `&mut` abandon and the durable commit used to escape with a
1067    /// bare `?`, leaving the abandon applied in memory while reporting
1068    /// failure (and never persisting it).
1069    #[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        // Accept a pending input so the abandon has staged work to mutate.
1077        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        // Inject a failure into the input-state snapshot step that runs after
1084        // the staged abandon mutation.
1085        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}