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