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_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    /// Snapshot + classify the lifecycle persistence payload, restoring the
144    /// caller's checkpoint on failure.
145    ///
146    /// Contract (Dogma K11): every fallible step between a staged `&mut` DSL
147    /// transition and the rollback-guarded durable commit restores the
148    /// caller's checkpoint. A bare `?` here would leave the staged lifecycle
149    /// live in driver state while reporting failure to the caller. The
150    /// checkpoint is returned on success so the durable commit arm can keep
151    /// using it.
152    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        // Contract: every fallible step between the staged DSL transition and
202        // the durable commit restores the caller's checkpoint on failure. A
203        // bare `?` here would leave the staged lifecycle (e.g. Destroy) live
204        // in driver state while reporting failure to the caller.
205        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    /// Low-level control projection shim for external contract tests.
263    ///
264    /// This does not decide lifecycle legality; it only applies an already
265    /// chosen MeerkatMachine control projection to the concrete driver shell.
266    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    /// Contract helper for external tests that need to start a run through the
285    /// same DSL authority used by the runtime loop.
286    #[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    /// Test-only authority override for crate-unit tests that need to seed
295    /// impossible or already-realized runtime phases.
296    #[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    /// Get pending events (delegates to inner).
309    pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
310        self.inner.drain_events()
311    }
312
313    /// Drain the typed post-admission signal (delegates to inner).
314    pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
315        self.inner.take_post_admission_signal()
316    }
317
318    /// Inspect the current typed post-admission signal without draining it.
319    pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
320        self.inner.post_admission_signal()
321    }
322
323    /// Check and clear wake flag (backward-compat, delegates to inner).
324    pub fn take_wake_requested(&mut self) -> bool {
325        self.inner.take_wake_requested()
326    }
327
328    /// Check and clear immediate processing flag (backward-compat, delegates to inner).
329    pub fn take_process_requested(&mut self) -> bool {
330        self.inner.take_process_requested()
331    }
332
333    /// Contract helper for recovery/queue-projection tests. Production runtime
334    /// execution must use generated batch authority via `dequeue_batch_exact`.
335    #[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    /// Apply input (delegates to inner).
511    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    /// Roll back staged inputs (delegates to inner).
520    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    /// Recycle the in-memory driver shell while preserving canonical pending
564    /// work from durable runtime truth.
565    ///
566    /// Unlike `reset()`, this must not abandon queued/staged work.
567    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        // Restore the checkpoint on classification failure: an early `?` here
599        // would leave the finalized retire state live without rollback.
600        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        // Restore the checkpoint on classification failure: an early `?` here
625        // would leave the reset-cleaned state live without rollback.
626        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        // Resolve the durable target BEFORE handing the checkpoint to the
663        // commit helper: an early `?` here would otherwise leave the staged
664        // destroy state live without restoring the checkpoint (driver-side
665        // shadow truth with no rollback).
666        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        // Resolve the durable target BEFORE handing the checkpoint to the
697        // commit helper, so a classification failure restores the staged
698        // executor-exit state instead of leaving it live without 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, "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 active_input_ids(&self) -> Vec<InputId> {
953        self.inner.active_input_ids()
954    }
955}
956
957#[cfg(test)]
958#[allow(clippy::unwrap_used, clippy::expect_used)]
959mod tests {
960    use super::*;
961    use chrono::Utc;
962    use meerkat_core::lifecycle::InputId;
963
964    fn make_prompt(text: &str) -> Input {
965        Input::Prompt(crate::input::PromptInput {
966            header: crate::input::InputHeader {
967                id: InputId::new(),
968                timestamp: Utc::now(),
969                source: crate::input::InputOrigin::Operator,
970                durability: crate::input::InputDurability::Durable,
971                visibility: crate::input::InputVisibility::default(),
972                idempotency_key: None,
973                supersession_key: None,
974                correlation_id: None,
975            },
976            content: text.into(),
977            typed_turn_appends: Vec::new(),
978            turn_metadata: None,
979        })
980    }
981
982    /// Dogma K11 (Persistent destroy / driver-side shadow truth): every
983    /// fallible step of `commit_lifecycle_with_rollback` AFTER the caller has
984    /// staged a DSL lifecycle transition must restore the caller's checkpoint.
985    /// The input-state snapshot read used to escape with a bare `?`, leaving
986    /// the staged lifecycle live in driver state while reporting failure.
987    #[tokio::test]
988    async fn commit_lifecycle_snapshot_failure_restores_checkpoint() {
989        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
990        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
991        let rid = LogicalRuntimeId::new("commit-lifecycle-rollback-contract");
992        let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
993
994        // Checkpoint BEFORE any state mutation (the caller's pre-stage view).
995        let checkpoint = driver.rollback_snapshot();
996
997        // Mutate driver state past the checkpoint (stands in for a staged
998        // Destroy/lifecycle transition awaiting durable commit).
999        let input = make_prompt("staged work");
1000        let input_id = input.id().clone();
1001        let outcome = driver.accept_input(input).await.unwrap();
1002        assert!(outcome.is_accepted());
1003        assert!(driver.input_phase(&input_id).is_some());
1004
1005        // Inject a failure into the input-state snapshot step.
1006        driver.force_input_snapshot_failure_for_test = true;
1007        let target_state = driver.inner_ref().runtime_state();
1008        let result = driver
1009            .commit_lifecycle_with_rollback(checkpoint, target_state, "test destroy")
1010            .await;
1011
1012        // The failure must propagate typed AND the staged driver state must be
1013        // rolled back to the checkpoint — no half-destroyed shadow truth.
1014        assert!(result.is_err(), "forced snapshot failure must propagate");
1015        assert!(
1016            driver.input_phase(&input_id).is_none(),
1017            "staged driver state must be restored to the pre-stage checkpoint"
1018        );
1019        assert!(driver.active_input_ids().is_empty());
1020    }
1021
1022    /// Same K11 checkpoint-restore contract for `abandon_pending_inputs`: the
1023    /// input-state snapshot / lifecycle-commit classification steps between
1024    /// the staged `&mut` abandon and the durable commit used to escape with a
1025    /// bare `?`, leaving the abandon applied in memory while reporting
1026    /// failure (and never persisting it).
1027    #[tokio::test]
1028    async fn abandon_pending_inputs_snapshot_failure_restores_checkpoint() {
1029        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1030        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1031        let rid = LogicalRuntimeId::new("abandon-rollback-contract");
1032        let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1033
1034        // Accept a pending input so the abandon has staged work to mutate.
1035        let input = make_prompt("pending work");
1036        let input_id = input.id().clone();
1037        let outcome = driver.accept_input(input).await.unwrap();
1038        assert!(outcome.is_accepted());
1039        assert!(driver.input_phase(&input_id).is_some());
1040
1041        // Inject a failure into the input-state snapshot step that runs after
1042        // the staged abandon mutation.
1043        driver.force_input_snapshot_failure_for_test = true;
1044        let result = driver
1045            .abandon_pending_inputs(InputAbandonReason::Reset)
1046            .await;
1047
1048        assert!(result.is_err(), "forced snapshot failure must propagate");
1049        assert!(
1050            driver.input_phase(&input_id).is_some(),
1051            "staged abandon must be rolled back: the pending input must still be live"
1052        );
1053    }
1054}