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::{
23    FencedInputStateBatchCasOutcome, InputStateBatchCasOutcome, MachineLifecycleCommit,
24    RuntimeStore, RuntimeStoreWriteFence,
25};
26use crate::traits::{DestroyReport, RecoveryReport, RuntimeDriver, RuntimeDriverError};
27
28use super::ephemeral::{
29    EphemeralDriverRollbackSnapshot, EphemeralRuntimeDriver, SharedIngressDslAuthority,
30};
31
32/// Persistent runtime driver — durable InputState via RuntimeStore.
33pub struct PersistentRuntimeDriver {
34    /// Underlying ephemeral driver for state machine logic.
35    inner: EphemeralRuntimeDriver,
36    /// Durable store for InputState + receipts.
37    store: Arc<dyn RuntimeStore>,
38    /// Blob store used to externalize durable input payloads.
39    blob_store: Arc<dyn BlobStore>,
40    /// Runtime ID for store operations.
41    runtime_id: LogicalRuntimeId,
42    /// Test-only fault injection: forces the input-state snapshot step of
43    /// [`Self::commit_lifecycle_with_rollback`] to fail so tests can pin the
44    /// checkpoint-restore contract for that arm.
45    #[cfg(test)]
46    pub(crate) force_input_snapshot_failure_for_test: bool,
47}
48
49impl PersistentRuntimeDriver {
50    pub(crate) async fn recover_inputs_after_runtime_authority(
51        &mut self,
52        recovered_unregister_progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
53    ) -> Result<RecoveryReport, RuntimeDriverError> {
54        let report = crate::meerkat_machine::machine_recover_persistent_inputs(
55            self.store.as_ref(),
56            &self.runtime_id,
57            &mut self.inner,
58            recovered_unregister_progress,
59        )
60        .await?;
61
62        let input_states = self.inner.authorized_stored_input_states_snapshot()?;
63        self.store
64            .persist_input_states_atomically(&self.runtime_id, &input_states)
65            .await
66            .map_err(|err| {
67                RuntimeDriverError::Internal(format!("recovered input persistence failed: {err}"))
68            })?;
69        Ok(report)
70    }
71
72    /// Recover durable input work and publish the normalized target image only
73    /// while both the original input rows and the caller's external authority
74    /// fence remain current.
75    pub(crate) async fn recover_inputs_after_runtime_authority_with_fence(
76        &mut self,
77        recovered_unregister_progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
78        write_fence: Arc<dyn RuntimeStoreWriteFence>,
79    ) -> Result<RecoveryReport, RuntimeDriverError> {
80        let observed =
81            crate::store::load_input_states_for_recovery(self.store.as_ref(), &self.runtime_id)
82                .await
83                .map_err(|error| RuntimeDriverError::RecoveryBackoff {
84                    reason: format!("failed to observe durable inputs for recovery: {error}"),
85                })?;
86        let report = crate::meerkat_machine::machine_recover_persistent_inputs_from_observed(
87            self.store.as_ref(),
88            &self.runtime_id,
89            &mut self.inner,
90            observed.clone(),
91            recovered_unregister_progress,
92        )
93        .await?;
94
95        let replacements = self.inner.authorized_stored_input_states_snapshot()?;
96        if replacements.is_empty() {
97            return Ok(report);
98        }
99        let mut expected = Vec::with_capacity(replacements.len());
100        for replacement in &replacements {
101            let input_id = &replacement.as_stored().state.input_id;
102            let Some(original) = observed
103                .iter()
104                .find(|candidate| &candidate.state.input_id == input_id)
105            else {
106                return Err(RuntimeDriverError::RecoveryCorruption {
107                    reason: format!(
108                        "recovered input {input_id} has no matching durable observation"
109                    ),
110                });
111            };
112            expected.push(original.clone());
113        }
114
115        let outcome = self
116            .store
117            .compare_and_swap_input_states_atomically_with_fence(
118                &self.runtime_id,
119                &expected,
120                &replacements,
121                write_fence,
122            )
123            .await
124            .map_err(|error| match error {
125                crate::store::RuntimeStoreError::Unsupported(reason) => {
126                    RuntimeDriverError::RecoveryRepairBlocked {
127                        evidence_digest: None,
128                        reason: format!(
129                            "runtime store lacks fenced input recovery capability: {reason}"
130                        ),
131                    }
132                }
133                other => RuntimeDriverError::RecoveryBackoff {
134                    reason: format!("fenced recovered input persistence failed: {other}"),
135                },
136            })?;
137        match outcome {
138            FencedInputStateBatchCasOutcome::Swapped => Ok(report),
139            FencedInputStateBatchCasOutcome::Stale => Err(RuntimeDriverError::StaleAuthority {
140                reason: "durable input state changed while cold recovery was preparing".to_string(),
141            }),
142            FencedInputStateBatchCasOutcome::FenceConflict { reason } => {
143                Err(RuntimeDriverError::StaleAuthority { reason })
144            }
145            FencedInputStateBatchCasOutcome::FenceBackoff { reason } => {
146                Err(RuntimeDriverError::RecoveryBackoff { reason })
147            }
148        }
149    }
150
151    /// Create a new persistent runtime driver.
152    pub fn new(
153        runtime_id: LogicalRuntimeId,
154        store: Arc<dyn RuntimeStore>,
155        blob_store: Arc<dyn BlobStore>,
156    ) -> Self {
157        Self::new_with_control(
158            runtime_id,
159            store,
160            blob_store,
161            Arc::new(StdRwLock::new(
162                crate::driver::ephemeral::RuntimeControlProjection::default(),
163            )),
164            crate::driver::ephemeral::new_ingress_dsl_authority(),
165        )
166    }
167
168    pub(crate) fn new_with_control(
169        runtime_id: LogicalRuntimeId,
170        store: Arc<dyn RuntimeStore>,
171        blob_store: Arc<dyn BlobStore>,
172        control: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
173        dsl: SharedIngressDslAuthority,
174    ) -> Self {
175        Self {
176            inner: EphemeralRuntimeDriver::new_with_control_and_dsl(
177                runtime_id.clone(),
178                control,
179                dsl,
180            ),
181            store,
182            blob_store,
183            runtime_id,
184            #[cfg(test)]
185            force_input_snapshot_failure_for_test: false,
186        }
187    }
188
189    /// Get immutable reference to the inner ephemeral driver.
190    pub fn inner_ref(&self) -> &EphemeralRuntimeDriver {
191        &self.inner
192    }
193
194    pub(crate) fn inner_mut(&mut self) -> &mut EphemeralRuntimeDriver {
195        &mut self.inner
196    }
197
198    #[cfg(test)]
199    pub(crate) async fn compare_and_swap_interaction_terminal_outbox_inputs(
200        &self,
201        expected: &[StoredInputState],
202        input_ids: &[InputId],
203    ) -> Result<InputStateBatchCasOutcome, RuntimeDriverError> {
204        let mut replacements = Vec::with_capacity(input_ids.len());
205        for input_id in input_ids {
206            let replacement = self
207                .inner
208                .authorized_stored_input_state(input_id)?
209                .ok_or_else(|| {
210                    RuntimeDriverError::Internal(format!(
211                        "interaction terminal outbox input {input_id} disappeared before compare-and-swap"
212                    ))
213                })?;
214            replacements.push(replacement);
215        }
216        self.store
217            .compare_and_swap_input_states_atomically(&self.runtime_id, expected, &replacements)
218            .await
219            .map_err(|error| {
220                RuntimeDriverError::Internal(format!(
221                    "interaction terminal outbox batch compare-and-swap failed: {error}"
222                ))
223            })
224    }
225
226    pub(crate) async fn compare_and_swap_interaction_terminal_outbox_replacements(
227        &self,
228        expected: &[StoredInputState],
229        replacements: &[crate::input_state::InputStatePersistenceRecord],
230    ) -> Result<InputStateBatchCasOutcome, RuntimeDriverError> {
231        self.store
232            .compare_and_swap_input_states_atomically(&self.runtime_id, expected, replacements)
233            .await
234            .map_err(|error| {
235                RuntimeDriverError::Internal(format!(
236                    "interaction terminal outbox batch compare-and-swap failed: {error}"
237                ))
238            })
239    }
240
241    pub(crate) async fn committed_session_snapshot_for_terminal_recovery(
242        &self,
243    ) -> Result<Option<Vec<u8>>, RuntimeDriverError> {
244        self.store
245            .load_session_snapshot(&self.runtime_id)
246            .await
247            .map_err(|error| {
248                RuntimeDriverError::Internal(format!(
249                    "interaction terminal recovery failed to load committed session snapshot: {error}"
250                ))
251            })
252    }
253
254    pub(crate) async fn durable_input_states_for_terminal_recovery(
255        &self,
256    ) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
257        crate::store::load_input_states_for_recovery(self.store.as_ref(), &self.runtime_id)
258            .await
259            .map_err(|error| {
260                RuntimeDriverError::Internal(format!(
261                    "interaction terminal recovery failed to load durable input states: {error}"
262                ))
263            })
264    }
265
266    pub(crate) fn rollback_snapshot(&self) -> EphemeralDriverRollbackSnapshot {
267        self.inner.rollback_snapshot()
268    }
269
270    pub(crate) fn restore_rollback_snapshot(&mut self, snapshot: EphemeralDriverRollbackSnapshot) {
271        self.inner.restore_rollback_snapshot(snapshot);
272    }
273
274    /// Get the logical runtime ID for this driver.
275    pub fn runtime_id(&self) -> &LogicalRuntimeId {
276        &self.runtime_id
277    }
278
279    pub(crate) async fn load_pending_compaction_projections(
280        &self,
281    ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeDriverError> {
282        self.store
283            .load_pending_compaction_projections(&self.runtime_id)
284            .await
285            .map_err(|error| {
286                RuntimeDriverError::Internal(format!(
287                    "failed to load compaction projection outbox: {error}"
288                ))
289            })
290    }
291
292    pub(crate) async fn mark_compaction_projection_finalized(
293        &self,
294        projection: &meerkat_core::CompactionProjectionId,
295    ) -> Result<(), RuntimeDriverError> {
296        self.store
297            .mark_compaction_projection_finalized(&self.runtime_id, projection)
298            .await
299            .map_err(|error| {
300                RuntimeDriverError::Internal(format!(
301                    "failed to finalize compaction projection outbox: {error}"
302                ))
303            })
304    }
305
306    pub(crate) async fn load_compaction_checkpoint_snapshot(
307        &self,
308    ) -> Result<Option<Vec<u8>>, RuntimeDriverError> {
309        self.store
310            .load_session_snapshot(&self.runtime_id)
311            .await
312            .map_err(|error| {
313                RuntimeDriverError::Internal(format!(
314                    "failed to load authoritative compaction checkpoint snapshot: {error}"
315                ))
316            })
317    }
318
319    pub(crate) async fn commit_compaction_checkpoint_snapshot(
320        &self,
321        session_snapshot: Vec<u8>,
322    ) -> Result<(), RuntimeDriverError> {
323        self.store
324            .commit_session_snapshot(
325                &self.runtime_id,
326                crate::store::SessionDelta { session_snapshot },
327            )
328            .await
329            .map_err(|error| {
330                RuntimeDriverError::Internal(format!(
331                    "failed to prepare authoritative compaction checkpoint snapshot: {error}"
332                ))
333            })
334    }
335
336    pub fn silent_comms_intents(&self) -> Vec<String> {
337        self.inner.silent_comms_intents()
338    }
339
340    /// Check if the runtime is idle (delegates to inner).
341    pub fn is_idle(&self) -> bool {
342        self.inner.is_idle()
343    }
344
345    /// Ask generated MeerkatMachine authority for the store-visible lifecycle.
346    fn runtime_state_for_persistence(&self) -> Result<RuntimeState, RuntimeDriverError> {
347        Self::runtime_state_for_persistence_from_inner(&self.inner)
348    }
349
350    fn runtime_state_for_persistence_from_inner(
351        inner: &EphemeralRuntimeDriver,
352    ) -> Result<RuntimeState, RuntimeDriverError> {
353        crate::meerkat_machine::classify_runtime_lifecycle_durable_state_with_pre_run_phase(
354            inner.runtime_state(),
355            inner.pre_run_phase(),
356        )
357        .map_err(|err| {
358            RuntimeDriverError::Internal(format!(
359                "generated runtime lifecycle durability classification failed: {err}"
360            ))
361        })
362    }
363
364    fn lifecycle_commit_for_persistence(
365        &self,
366    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
367        Self::lifecycle_commit_for_persistence_from_inner(&self.inner)
368    }
369
370    fn lifecycle_commit_for_persistence_with_supervisor_authority(
371        &self,
372        supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
373    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
374        Ok(
375            MachineLifecycleCommit::new_with_binding_and_unregister_progress(
376                Self::runtime_state_for_persistence_from_inner(&self.inner)?,
377                self.inner.machine_lifecycle_binding_facts(),
378                supervisor_authority,
379                Self::unregister_progress_for_persistence_from_inner(&self.inner),
380            ),
381        )
382    }
383
384    fn lifecycle_commit_for_persistence_from_inner(
385        inner: &EphemeralRuntimeDriver,
386    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
387        Ok(
388            MachineLifecycleCommit::new_with_binding_and_unregister_progress(
389                Self::runtime_state_for_persistence_from_inner(inner)?,
390                inner.machine_lifecycle_binding_facts(),
391                inner.supervisor_authority_snapshot(),
392                Self::unregister_progress_for_persistence_from_inner(inner),
393            ),
394        )
395    }
396
397    /// Project a committed final `UnregisterSession` for durable storage.
398    ///
399    /// The live entry deliberately keeps `registration_phase = Draining` as a
400    /// same-process rematerialization tombstone until exact entry removal. That
401    /// mechanical fence is not durable unregister progress: final generated
402    /// authority has cleared the session binding and all drain obligations, so
403    /// persisting a progress row would make a later process replay a completed
404    /// teardown and reject fresh registration.
405    fn lifecycle_commit_for_completed_unregister(
406        &self,
407    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
408        let completed = {
409            let authority = self.inner.shared_dsl_authority();
410            let authority = authority
411                .lock()
412                .unwrap_or_else(std::sync::PoisonError::into_inner);
413            let state = authority.state();
414            state.registration_phase == crate::meerkat_machine::dsl::RegistrationPhase::Draining
415                && state.session_id.is_none()
416                && state.active_runtime_id.is_none()
417                && state.active_fence_token.is_none()
418                && state.active_runtime_generation.is_none()
419                && state.active_runtime_epoch_id.is_none()
420                && !state.unregister_runtime_loop_drain_pending
421                && !state.unregister_comms_drain_exit_pending
422                && !state.unregister_completion_waiter_drain_pending
423        };
424        if !completed {
425            return Err(RuntimeDriverError::Internal(
426                "completed unregister persistence requires the generated final lifecycle image"
427                    .to_string(),
428            ));
429        }
430        Ok(
431            MachineLifecycleCommit::new_with_binding_and_unregister_progress(
432                Self::runtime_state_for_persistence_from_inner(&self.inner)?,
433                self.inner.machine_lifecycle_binding_facts(),
434                self.inner.supervisor_authority_snapshot(),
435                None,
436            ),
437        )
438    }
439
440    fn unregister_progress_for_persistence_from_inner(
441        inner: &EphemeralRuntimeDriver,
442    ) -> Option<crate::store::MachineUnregisterProgressSnapshot> {
443        let authority = inner.shared_dsl_authority();
444        let authority = authority
445            .lock()
446            .unwrap_or_else(std::sync::PoisonError::into_inner);
447        let state = authority.state();
448        (state.registration_phase == crate::meerkat_machine::dsl::RegistrationPhase::Draining).then(
449            || {
450                crate::store::MachineUnregisterProgressSnapshot::new(
451                    state.unregister_runtime_loop_drain_pending,
452                    state.unregister_comms_drain_exit_pending,
453                    state.unregister_completion_waiter_drain_pending,
454                    state.unregister_runtime_loop_forced_abort,
455                    state.unregister_comms_drain_forced_abort,
456                )
457            },
458        )
459    }
460
461    /// Snapshot + classify the lifecycle persistence payload, restoring the
462    /// caller's checkpoint on failure.
463    ///
464    /// Contract (Dogma K11): every fallible step between a staged `&mut` DSL
465    /// transition and the rollback-guarded durable commit restores the
466    /// caller's checkpoint. A bare `?` here would leave the staged lifecycle
467    /// live in driver state while reporting failure to the caller. The
468    /// checkpoint is returned on success so the durable commit arm can keep
469    /// using it.
470    fn lifecycle_persistence_payload_with_rollback(
471        &mut self,
472        checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
473        context: &str,
474    ) -> Result<
475        (
476            super::ephemeral::EphemeralDriverRollbackSnapshot,
477            Vec<InputStatePersistenceRecord>,
478            MachineLifecycleCommit,
479        ),
480        RuntimeDriverError,
481    > {
482        let input_states_result = self.inner.authorized_stored_input_states_snapshot();
483        #[cfg(test)]
484        let input_states_result = if self.force_input_snapshot_failure_for_test {
485            Err(RuntimeDriverError::Internal(
486                "forced input-state snapshot failure for checkpoint-restore contract test"
487                    .to_string(),
488            ))
489        } else {
490            input_states_result
491        };
492        let input_states = match input_states_result {
493            Ok(input_states) => input_states,
494            Err(err) => {
495                self.inner.restore_rollback_snapshot(checkpoint);
496                return Err(RuntimeDriverError::Internal(format!(
497                    "{context} input-state snapshot failed: {err}"
498                )));
499            }
500        };
501        let commit = match self.lifecycle_commit_for_persistence() {
502            Ok(commit) => commit,
503            Err(err) => {
504                self.inner.restore_rollback_snapshot(checkpoint);
505                return Err(RuntimeDriverError::Internal(format!(
506                    "{context} lifecycle commit classification failed: {err}"
507                )));
508            }
509        };
510        Ok((checkpoint, input_states, commit))
511    }
512
513    async fn commit_lifecycle_with_rollback(
514        &mut self,
515        checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
516        target_state: RuntimeState,
517        context: &str,
518    ) -> Result<(), RuntimeDriverError> {
519        // Contract: every fallible step between the staged DSL transition and
520        // the durable commit restores the caller's checkpoint on failure. A
521        // bare `?` here would leave the staged lifecycle (e.g. Destroy) live
522        // in driver state while reporting failure to the caller.
523        let (checkpoint, input_states, commit) =
524            self.lifecycle_persistence_payload_with_rollback(checkpoint, context)?;
525        let target_durable_state =
526            match crate::meerkat_machine::classify_runtime_lifecycle_durable_state_with_pre_run_phase(
527                target_state,
528                self.inner.pre_run_phase(),
529            ) {
530                Ok(target_durable_state) => target_durable_state,
531                Err(err) => {
532                    self.inner.restore_rollback_snapshot(checkpoint);
533                    return Err(RuntimeDriverError::Internal(format!(
534                        "{context} generated target lifecycle durability classification failed: {err}"
535                    )));
536                }
537            };
538        if commit.runtime_state() != target_durable_state {
539            self.inner.restore_rollback_snapshot(checkpoint);
540            return Err(RuntimeDriverError::Internal(format!(
541                "{context} durable persist target {target_durable_state:?} from live {target_state:?} disagreed with generated lifecycle commit {:?}",
542                commit.runtime_state()
543            )));
544        }
545        if let Err(err) = self
546            .store
547            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
548            .await
549        {
550            self.inner.restore_rollback_snapshot(checkpoint);
551            return Err(RuntimeDriverError::Internal(format!(
552                "{context} persist failed: {err}"
553            )));
554        }
555        Ok(())
556    }
557
558    pub(crate) async fn publish_service_turn_terminal(
559        &mut self,
560        checkpoint: super::ephemeral::EphemeralDriverRollbackSnapshot,
561        target_state: RuntimeState,
562        session_snapshot: Vec<u8>,
563        receipt: meerkat_core::lifecycle::RunBoundaryReceipt,
564        owner_session_id: meerkat_core::types::SessionId,
565    ) -> Result<(), RuntimeDriverError> {
566        let commit = match self.lifecycle_commit_for_persistence() {
567            Ok(commit) => commit,
568            Err(error) => {
569                self.inner.restore_rollback_snapshot(checkpoint);
570                return Err(RuntimeDriverError::Internal(format!(
571                    "service turn terminal receipt lifecycle classification failed: {error}"
572                )));
573            }
574        };
575        let target_durable_state =
576            match crate::meerkat_machine::classify_runtime_lifecycle_durable_state(target_state) {
577                Ok(target_durable_state) => target_durable_state,
578                Err(error) => {
579                    self.inner.restore_rollback_snapshot(checkpoint);
580                    return Err(RuntimeDriverError::Internal(format!(
581                        "service turn terminal receipt target classification failed: {error}"
582                    )));
583                }
584            };
585        if commit.runtime_state() != target_durable_state {
586            self.inner.restore_rollback_snapshot(checkpoint);
587            return Err(RuntimeDriverError::Internal(format!(
588                "service turn terminal receipt durable target {target_durable_state:?} disagreed with generated lifecycle {:?}",
589                commit.runtime_state()
590            )));
591        }
592        if let Err(error) = self
593            .store
594            .atomic_apply_with_machine_lifecycle(
595                &self.runtime_id,
596                crate::store::SessionDelta { session_snapshot },
597                receipt,
598                commit,
599                Vec::new(),
600                owner_session_id,
601            )
602            .await
603        {
604            self.inner.restore_rollback_snapshot(checkpoint);
605            return Err(RuntimeDriverError::Internal(format!(
606                "service turn terminal receipt persist failed: {error}"
607            )));
608        }
609        self.inner.sync_control_projection_from_dsl_authority();
610        Ok(())
611    }
612
613    pub(crate) fn set_control_projection(
614        &mut self,
615        next_phase: RuntimeState,
616        current_run_id: Option<RunId>,
617        pre_run_phase: Option<RuntimeState>,
618    ) {
619        self.inner
620            .set_control_projection(next_phase, current_run_id, pre_run_phase);
621    }
622
623    /// Low-level control projection shim for external contract tests.
624    ///
625    /// This does not decide lifecycle legality; it only applies an already
626    /// chosen MeerkatMachine control projection to the concrete driver shell.
627    pub(crate) fn sync_control_projection_from_dsl_authority(&mut self) {
628        self.inner.sync_control_projection_from_dsl_authority();
629    }
630
631    pub(crate) async fn persist_current_machine_lifecycle(
632        &mut self,
633        context: &str,
634    ) -> Result<(), RuntimeDriverError> {
635        let input_states = self.inner.authorized_stored_input_states_snapshot()?;
636        let commit = self.lifecycle_commit_for_persistence()?;
637        self.store
638            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
639            .await
640            .map_err(|err| {
641                RuntimeDriverError::Internal(format!("{context} lifecycle persist failed: {err}"))
642            })
643    }
644
645    pub(crate) async fn commit_unregister_finalization(
646        &mut self,
647        context: &str,
648        retired_ops_epoch: &meerkat_core::RuntimeEpochId,
649        authority: crate::meerkat_machine::DeleteOpsFinalizationAuthority,
650    ) -> Result<(), RuntimeDriverError> {
651        let input_states = self.inner.authorized_stored_input_states_snapshot()?;
652        let commit = self.lifecycle_commit_for_completed_unregister()?;
653        let finalization = crate::store::UnregisterFinalizationCommit::new(
654            commit,
655            input_states,
656            retired_ops_epoch.clone(),
657            authority,
658        );
659        self.store
660            .commit_unregister_finalization(&self.runtime_id, finalization)
661            .await
662            .map_err(|err| match err {
663                crate::store::RuntimeStoreError::UnregisterFinalizationOutcomeUnknown(reason) => {
664                    RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
665                        reason: format!("{context} lifecycle+ops finalization: {reason}"),
666                    }
667                }
668                err => RuntimeDriverError::Internal(format!(
669                    "{context} lifecycle+ops finalization failed: {err}"
670                )),
671            })
672    }
673
674    pub(crate) async fn persist_completed_unregister_machine_lifecycle(
675        &mut self,
676        context: &str,
677        _authority: crate::meerkat_machine::RetainOpsFinalizationAuthority,
678    ) -> Result<(), RuntimeDriverError> {
679        let input_states = self.inner.authorized_stored_input_states_snapshot()?;
680        let commit = self.lifecycle_commit_for_completed_unregister()?;
681        self.store
682            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
683            .await
684            .map_err(|error| {
685                // The generic lifecycle commit contract is atomic, but unlike
686                // commit_unregister_finalization it does not distinguish a
687                // definitely-uncommitted error from a lost acknowledgement.
688                // RetainSnapshot finalization must therefore treat every
689                // error as ambiguous: rolling local authority back to
690                // Draining could overwrite a terminal image that already
691                // committed durably.
692                RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
693                    reason: format!(
694                        "{context} retained lifecycle finalization acknowledgement unavailable: {error}"
695                    ),
696                }
697            })
698    }
699
700    /// Persist a previewed closed supervisor projection alongside the current
701    /// machine lifecycle. This lets the supervisor saga commit durable truth
702    /// before changing the shared live authority, avoiding a whole-authority
703    /// rollback across asynchronous store I/O (peer ingress may concurrently
704    /// mutate unrelated generated fields).
705    pub(crate) async fn persist_current_machine_lifecycle_with_supervisor_authority(
706        &mut self,
707        context: &str,
708        supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
709    ) -> Result<(), RuntimeDriverError> {
710        let input_states = self.inner.authorized_stored_input_states_snapshot()?;
711        let commit =
712            self.lifecycle_commit_for_persistence_with_supervisor_authority(supervisor_authority)?;
713        self.store
714            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
715            .await
716            .map_err(|err| {
717                RuntimeDriverError::Internal(format!("{context} lifecycle persist failed: {err}"))
718            })
719    }
720
721    /// Contract helper for external tests that need to start a run through the
722    /// same DSL authority used by the runtime loop.
723    #[doc(hidden)]
724    pub fn contract_begin_run_authority(
725        &mut self,
726        run_id: RunId,
727    ) -> Result<(), RuntimeDriverError> {
728        self.inner.contract_begin_run_authority(run_id)
729    }
730
731    /// Get pending events (delegates to inner).
732    pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
733        self.inner.drain_events()
734    }
735
736    /// Drain the typed post-admission signal (delegates to inner).
737    pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
738        self.inner.take_post_admission_signal()
739    }
740
741    /// Inspect the current typed post-admission signal without draining it.
742    pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
743        self.inner.post_admission_signal()
744    }
745
746    /// Check and clear wake flag (backward-compat, delegates to inner).
747    pub fn take_wake_requested(&mut self) -> bool {
748        self.inner.take_wake_requested()
749    }
750
751    /// Check and clear immediate processing flag (backward-compat, delegates to inner).
752    pub fn take_process_requested(&mut self) -> bool {
753        self.inner.take_process_requested()
754    }
755
756    /// Contract helper for recovery/queue-projection tests. Production runtime
757    /// execution must use generated batch authority via `dequeue_batch_exact`.
758    #[cfg(any(test, debug_assertions, feature = "test-support"))]
759    #[doc(hidden)]
760    pub fn contract_dequeue_next_for_recovery_tests(&mut self) -> Option<(InputId, Input)> {
761        self.inner.contract_dequeue_next_for_recovery_tests()
762    }
763
764    pub(crate) fn dequeue_batch_exact(
765        &mut self,
766        batch: &crate::meerkat_machine::driver::AuthorizedRuntimeLoopBatch,
767    ) -> Result<Vec<(InputId, Input)>, RuntimeDriverError> {
768        self.inner.dequeue_batch_exact(batch)
769    }
770
771    pub fn has_queued_input_outside(&self, excluded: &[InputId]) -> bool {
772        self.inner.has_queued_input_outside(excluded)
773    }
774
775    pub(crate) fn defer_queued_inputs_behind_backlog(
776        &mut self,
777        input_ids: &[InputId],
778    ) -> Result<(), RuntimeDriverError> {
779        self.inner.defer_queued_inputs_behind_backlog(input_ids)
780    }
781
782    pub(crate) fn absorb_post_admission_effects(
783        &mut self,
784        effects: &[crate::meerkat_machine::dsl::MeerkatMachineEffect],
785    ) {
786        self.inner.absorb_post_admission_effects(effects);
787    }
788
789    pub(crate) fn resolve_admission(
790        &self,
791        input: &Input,
792    ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
793        self.inner.resolve_admission(input)
794    }
795
796    pub(crate) fn resolve_admission_with_active_turn_boundary(
797        &self,
798        input: &Input,
799        active_turn_boundary_available: bool,
800    ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
801        self.inner
802            .resolve_admission_with_active_turn_boundary(input, active_turn_boundary_available)
803    }
804
805    pub(crate) fn resolve_admission_without_wake_with_active_turn_boundary(
806        &self,
807        input: &Input,
808        active_turn_boundary_available: bool,
809    ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
810        self.inner
811            .resolve_admission_without_wake_with_active_turn_boundary(
812                input,
813                active_turn_boundary_available,
814            )
815    }
816
817    pub(crate) async fn accept_resolved_input(
818        &mut self,
819        input: Input,
820        resolved: crate::accept::ResolvedAdmission,
821    ) -> Result<AcceptOutcome, RuntimeDriverError> {
822        let mut staged = self.inner.clone_with_isolated_dsl_authority();
823        staged.ensure_contract_session_authority()?;
824        let staged_resolved = if resolved.authority().without_wake() {
825            staged.resolve_admission_without_wake_with_active_turn_boundary(
826                &input,
827                resolved.authority().active_turn_boundary_available(),
828            )?
829        } else {
830            staged.resolve_admission_with_active_turn_boundary(
831                &input,
832                resolved.authority().active_turn_boundary_available(),
833            )?
834        };
835        if !resolved.semantically_equivalent_to(&staged_resolved) {
836            return Err(RuntimeDriverError::Internal(format!(
837                "staged admission resolution diverged from preview: preview={resolved:?}, staged={staged_resolved:?}"
838            )));
839        }
840        let flags = staged_resolved.coarse_flags();
841        let staged_outcome = staged
842            .accept_resolved_input(input.clone(), staged_resolved)
843            .await?;
844
845        let AcceptOutcome::Accepted {
846            input_id: staged_input_id,
847            ..
848        } = staged_outcome
849        else {
850            return self.inner.accept_resolved_input(input, resolved).await;
851        };
852
853        staged.machine_apply_accept_with_completion_signal(&staged_input_id, flags)?;
854        let Some(mut staged_bundle) = staged.stored_input_state(&staged_input_id) else {
855            return Err(RuntimeDriverError::Internal(format!(
856                "generated input lifecycle phase missing for accepted input {staged_input_id}"
857            )));
858        };
859        let mut input_for_recovery = input.clone();
860        externalize_input_images(self.blob_store.as_ref(), &mut input_for_recovery)
861            .await
862            .map_err(|err| {
863                RuntimeDriverError::Internal(format!(
864                    "failed to externalize runtime input images: {err}"
865                ))
866            })?;
867        staged_bundle.state.persisted_input = Some(input_for_recovery.clone());
868        self.persist_state(&staged_bundle).await?;
869
870        self.inner.ensure_contract_session_authority()?;
871        let mut outcome = self.inner.accept_resolved_input(input, resolved).await?;
872        if let AcceptOutcome::Accepted {
873            ref input_id,
874            ref mut state,
875            ref mut seed,
876            ..
877        } = outcome
878        {
879            if input_id != &staged_input_id {
880                return Err(RuntimeDriverError::Internal(format!(
881                    "staged accepted input {staged_input_id} differed from committed input {input_id}"
882                )));
883            }
884            self.inner
885                .machine_apply_accept_with_completion_signal(input_id, flags)?;
886            let Some(mut bundle) = self.inner.stored_input_state(input_id) else {
887                return Err(RuntimeDriverError::Internal(format!(
888                    "generated input lifecycle phase missing for accepted input {input_id}"
889                )));
890            };
891            bundle.state.persisted_input = Some(input_for_recovery);
892            self.inner.ledger_mut().accept(bundle.state.clone());
893            *state = bundle.state;
894            *seed = bundle.seed;
895        }
896
897        Ok(outcome)
898    }
899
900    pub(crate) async fn preview_accept_resolved_input(
901        &self,
902        input: Input,
903        resolved: &crate::accept::ResolvedAdmission,
904    ) -> Result<AcceptOutcome, RuntimeDriverError> {
905        let mut staged = self.inner.clone_with_isolated_dsl_authority();
906        staged.ensure_contract_session_authority()?;
907        let staged_resolved = if resolved.authority().without_wake() {
908            staged.resolve_admission_without_wake_with_active_turn_boundary(
909                &input,
910                resolved.authority().active_turn_boundary_available(),
911            )?
912        } else {
913            staged.resolve_admission_with_active_turn_boundary(
914                &input,
915                resolved.authority().active_turn_boundary_available(),
916            )?
917        };
918        if !resolved.semantically_equivalent_to(&staged_resolved) {
919            return Err(RuntimeDriverError::Internal(format!(
920                "staged admission preview diverged from caller resolution: preview={resolved:?}, staged={staged_resolved:?}"
921            )));
922        }
923        staged.accept_resolved_input(input, staged_resolved).await
924    }
925
926    pub(crate) fn machine_realize_authorized_stage_batch(
927        &mut self,
928        authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
929    ) -> Result<(), crate::traits::RuntimeDriverError> {
930        self.inner.machine_realize_authorized_stage_batch(authority)
931    }
932
933    /// Apply input (delegates to inner).
934    pub fn apply_input(
935        &mut self,
936        input_id: &InputId,
937        run_id: &meerkat_core::lifecycle::RunId,
938    ) -> Result<(), crate::traits::RuntimeDriverError> {
939        self.inner.apply_input(input_id, run_id)
940    }
941
942    pub(crate) fn machine_realize_terminal_failure_applied(
943        &mut self,
944        run_id: &meerkat_core::lifecycle::RunId,
945        input_ids: &[InputId],
946    ) -> Result<(), crate::traits::RuntimeDriverError> {
947        self.inner
948            .machine_realize_terminal_failure_applied(run_id, input_ids)
949    }
950
951    /// Roll back staged inputs (delegates to inner).
952    pub fn rollback_staged(
953        &mut self,
954        input_ids: &[InputId],
955    ) -> Result<(), crate::traits::RuntimeDriverError> {
956        self.inner.rollback_staged(input_ids)
957    }
958
959    async fn persist_state(&self, state: &StoredInputState) -> Result<(), RuntimeDriverError> {
960        let state = InputStatePersistenceRecord::from_machine_snapshot(state.clone())
961            .map_err(RuntimeDriverError::Internal)?;
962        self.store
963            .persist_input_state(&self.runtime_id, &state)
964            .await
965            .map_err(|e| RuntimeDriverError::Internal(e.to_string()))
966    }
967
968    pub(crate) async fn abandon_pending_inputs(
969        &mut self,
970        reason: InputAbandonReason,
971    ) -> Result<usize, RuntimeDriverError> {
972        let checkpoint = self.inner.rollback_snapshot();
973        let abandoned = match self.inner.abandon_pending_inputs(reason) {
974            Ok(abandoned) => abandoned,
975            Err(err) => {
976                self.inner.restore_rollback_snapshot(checkpoint);
977                return Err(err);
978            }
979        };
980        let (checkpoint, input_states, commit) =
981            self.lifecycle_persistence_payload_with_rollback(checkpoint, "pending input abandon")?;
982        if let Err(err) = self
983            .store
984            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
985            .await
986        {
987            self.inner.restore_rollback_snapshot(checkpoint);
988            return Err(RuntimeDriverError::Internal(format!(
989                "pending input abandon persist failed: {err}"
990            )));
991        }
992        Ok(abandoned)
993    }
994
995    pub(crate) async fn abandon_queued_input(
996        &mut self,
997        input_id: &meerkat_core::lifecycle::InputId,
998        reason: InputAbandonReason,
999    ) -> Result<bool, RuntimeDriverError> {
1000        let checkpoint = self.inner.rollback_snapshot();
1001        let abandoned = match self.inner.abandon_queued_input(input_id, reason) {
1002            Ok(abandoned) => abandoned,
1003            Err(error) => {
1004                self.inner.restore_rollback_snapshot(checkpoint);
1005                return Err(error);
1006            }
1007        };
1008        if !abandoned {
1009            return Ok(false);
1010        }
1011        let (checkpoint, input_states, commit) =
1012            self.lifecycle_persistence_payload_with_rollback(checkpoint, "tracked input cancel")?;
1013        if let Err(error) = self
1014            .store
1015            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1016            .await
1017        {
1018            self.inner.restore_rollback_snapshot(checkpoint);
1019            return Err(RuntimeDriverError::Internal(format!(
1020                "tracked input cancel persist failed: {error}"
1021            )));
1022        }
1023        Ok(true)
1024    }
1025
1026    /// Recycle the in-memory driver shell while preserving canonical pending
1027    /// work from durable runtime truth.
1028    ///
1029    /// Unlike `reset()`, this must not abandon queued/staged work.
1030    pub(crate) async fn recycle_preserving_work(&mut self) -> Result<usize, RuntimeDriverError> {
1031        let checkpoint = self.inner.rollback_snapshot();
1032        let transferred = match self.inner.recycle_preserving_work() {
1033            Ok(transferred) => transferred,
1034            Err(err) => {
1035                self.inner.restore_rollback_snapshot(checkpoint);
1036                return Err(err);
1037            }
1038        };
1039        let (checkpoint, input_states, commit) =
1040            self.lifecycle_persistence_payload_with_rollback(checkpoint, "recycle")?;
1041        if let Err(err) = self
1042            .store
1043            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1044            .await
1045        {
1046            self.inner.restore_rollback_snapshot(checkpoint);
1047            return Err(RuntimeDriverError::Internal(format!(
1048                "recycle persist failed: {err}"
1049            )));
1050        }
1051
1052        self.inner.sync_control_projection_from_dsl_authority();
1053        Ok(transferred)
1054    }
1055
1056    pub(crate) async fn realize_retire_lifecycle(
1057        &mut self,
1058    ) -> Result<crate::traits::RetireReport, RuntimeDriverError> {
1059        let checkpoint = self.inner.rollback_snapshot();
1060        let report = self.inner.finalize_retire();
1061        // Restore the checkpoint on classification failure: an early `?` here
1062        // would leave the finalized retire state live without rollback.
1063        let target_state = match self.runtime_state_for_persistence() {
1064            Ok(target_state) => target_state,
1065            Err(err) => {
1066                self.inner.restore_rollback_snapshot(checkpoint);
1067                return Err(err);
1068            }
1069        };
1070        self.commit_lifecycle_with_rollback(checkpoint, target_state, "retire")
1071            .await?;
1072        self.inner.sync_control_projection_from_dsl_authority();
1073        Ok(report)
1074    }
1075
1076    pub(crate) async fn realize_reset_lifecycle(
1077        &mut self,
1078    ) -> Result<crate::traits::ResetReport, RuntimeDriverError> {
1079        let checkpoint = self.inner.rollback_snapshot();
1080        let report = match self.inner.reset_cleanup() {
1081            Ok(report) => report,
1082            Err(err) => {
1083                self.inner.restore_rollback_snapshot(checkpoint);
1084                return Err(err);
1085            }
1086        };
1087        // Restore the checkpoint on classification failure: an early `?` here
1088        // would leave the reset-cleaned state live without rollback.
1089        let target_state = match self.runtime_state_for_persistence() {
1090            Ok(target_state) => target_state,
1091            Err(err) => {
1092                self.inner.restore_rollback_snapshot(checkpoint);
1093                return Err(err);
1094            }
1095        };
1096        self.commit_lifecycle_with_rollback(checkpoint, target_state, "reset")
1097            .await?;
1098        self.inner.sync_control_projection_from_dsl_authority();
1099        Ok(report)
1100    }
1101
1102    pub(crate) fn prepare_destroy_lifecycle(
1103        &mut self,
1104    ) -> Result<(EphemeralDriverRollbackSnapshot, DestroyReport), RuntimeDriverError> {
1105        let checkpoint = self.inner.rollback_snapshot();
1106        let abandoned = match self.inner.destroy_cleanup() {
1107            Ok(abandoned) => abandoned,
1108            Err(err) => {
1109                self.inner.restore_rollback_snapshot(checkpoint);
1110                return Err(err);
1111            }
1112        };
1113        Ok((
1114            checkpoint,
1115            DestroyReport {
1116                inputs_abandoned: abandoned,
1117            },
1118        ))
1119    }
1120
1121    pub(crate) async fn commit_prepared_destroy_lifecycle(
1122        &mut self,
1123        checkpoint: EphemeralDriverRollbackSnapshot,
1124    ) -> Result<(), RuntimeDriverError> {
1125        // Resolve the durable target BEFORE handing the checkpoint to the
1126        // commit helper: an early `?` here would otherwise leave the staged
1127        // destroy state live without restoring the checkpoint (driver-side
1128        // shadow truth with no rollback).
1129        let target_state = match self.runtime_state_for_persistence() {
1130            Ok(target_state) => target_state,
1131            Err(err) => {
1132                self.inner.restore_rollback_snapshot(checkpoint);
1133                return Err(err);
1134            }
1135        };
1136        self.commit_lifecycle_with_rollback(checkpoint, target_state, "destroy")
1137            .await
1138    }
1139
1140    pub(crate) fn rollback_prepared_destroy_lifecycle(
1141        &mut self,
1142        checkpoint: EphemeralDriverRollbackSnapshot,
1143    ) {
1144        self.inner.restore_rollback_snapshot(checkpoint);
1145    }
1146
1147    pub(crate) async fn finalize_runtime_executor_exit(
1148        &mut self,
1149    ) -> Result<(), RuntimeDriverError> {
1150        let checkpoint = self.inner.rollback_snapshot();
1151        if let Err(err) = self.inner.apply_runtime_executor_exited_authority() {
1152            self.inner.restore_rollback_snapshot(checkpoint);
1153            return Err(err);
1154        }
1155        if let Err(err) = self.inner.stop_runtime_cleanup() {
1156            self.inner.restore_rollback_snapshot(checkpoint);
1157            return Err(err);
1158        }
1159        // Resolve the durable target BEFORE handing the checkpoint to the
1160        // commit helper, so a classification failure restores the staged
1161        // executor-exit state instead of leaving it live without rollback.
1162        let target_state = match self.runtime_state_for_persistence() {
1163            Ok(target_state) => target_state,
1164            Err(err) => {
1165                self.inner.restore_rollback_snapshot(checkpoint);
1166                return Err(err);
1167            }
1168        };
1169        self.commit_lifecycle_with_rollback(checkpoint, target_state, "stop")
1170            .await?;
1171        self.inner.sync_control_projection_from_dsl_authority();
1172        Ok(())
1173    }
1174
1175    pub(crate) fn machine_realize_boundary_applied_in_memory(
1176        &mut self,
1177        run_id: &RunId,
1178        receipt: &RunBoundaryReceipt,
1179    ) -> Result<(), RuntimeDriverError> {
1180        self.inner.machine_realize_boundary_applied(run_id, receipt)
1181    }
1182
1183    pub(crate) fn machine_realize_run_completed_in_memory(
1184        &mut self,
1185        run_id: &RunId,
1186        consumed_input_ids: &[InputId],
1187    ) -> Result<(), RuntimeDriverError> {
1188        self.inner
1189            .machine_realize_run_completed(run_id, consumed_input_ids)
1190    }
1191
1192    pub(crate) async fn machine_realize_live_boundary_context_injected(
1193        &mut self,
1194        run_id: &RunId,
1195        input_ids: &[InputId],
1196        stage_authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
1197        session_snapshot: Option<Vec<u8>>,
1198        owner_session_id: &meerkat_core::types::SessionId,
1199    ) -> Result<(), RuntimeDriverError> {
1200        let checkpoint = self.inner.rollback_snapshot();
1201        let receipt = match self.inner.machine_realize_live_boundary_context_injected(
1202            run_id,
1203            input_ids,
1204            stage_authority,
1205        ) {
1206            Ok(receipt) => receipt,
1207            Err(err) => {
1208                self.inner.restore_rollback_snapshot(checkpoint);
1209                return Err(err);
1210            }
1211        };
1212        let input_updates = match self.inner.authorized_stored_input_states_snapshot() {
1213            Ok(input_updates) => input_updates,
1214            Err(err) => {
1215                self.inner.restore_rollback_snapshot(checkpoint);
1216                return Err(err);
1217            }
1218        };
1219        if let Err(err) = self
1220            .store
1221            .atomic_apply(
1222                &self.runtime_id,
1223                session_snapshot
1224                    .map(|session_snapshot| crate::store::SessionDelta { session_snapshot }),
1225                receipt.clone(),
1226                input_updates,
1227                Some(owner_session_id.clone()),
1228            )
1229            .await
1230        {
1231            self.inner.restore_rollback_snapshot(checkpoint);
1232            return Err(RuntimeDriverError::Internal(format!(
1233                "runtime live-boundary context commit failed: {err}"
1234            )));
1235        }
1236        Ok(())
1237    }
1238
1239    pub(crate) async fn machine_commit_completed_boundary_snapshot(
1240        &mut self,
1241        receipt: &RunBoundaryReceipt,
1242        session_snapshot: Option<Vec<u8>>,
1243        owner_session_id: &meerkat_core::types::SessionId,
1244    ) -> Result<(), RuntimeDriverError> {
1245        let input_updates = self.inner.authorized_stored_input_states_snapshot()?;
1246        self.store
1247            .atomic_apply(
1248                &self.runtime_id,
1249                session_snapshot
1250                    .map(|session_snapshot| crate::store::SessionDelta { session_snapshot }),
1251                receipt.clone(),
1252                input_updates,
1253                Some(owner_session_id.clone()),
1254            )
1255            .await
1256            .map_err(|e| {
1257                RuntimeDriverError::Internal(format!(
1258                    "runtime completed-boundary commit failed: {e}"
1259                ))
1260            })
1261    }
1262
1263    /// Persist a failed-run realization whose generated input transitions and
1264    /// directed terminal outboxes have already been staged in `inner` by the
1265    /// shared `DriverEntry` owner. Keeping this persistence step after the
1266    /// shared realization makes the queued/abandoned split and its exact
1267    /// terminal recipient batch one atomic store commit.
1268    pub(crate) async fn persist_machine_realized_run_failed(
1269        &mut self,
1270        realization: crate::meerkat_machine::driver::MachineRunFailureRealization,
1271    ) -> Result<(), RuntimeDriverError> {
1272        let crate::meerkat_machine::driver::MachineRunFailureRealization {
1273            run_id,
1274            contributing_input_ids,
1275            replay_plan,
1276            terminal_error,
1277            runtime_apply_failure,
1278            recoverable,
1279            applied_commit,
1280        } = realization;
1281        let checkpoint = self.inner.rollback_snapshot();
1282        let failure_cause = runtime_apply_failure.as_ref().map(|failure| failure.kind);
1283        tracing::debug!(
1284            run_id = ?run_id,
1285            contributors = contributing_input_ids.len(),
1286            replay_kind = replay_plan.notice_kind,
1287            recoverable,
1288            error = terminal_error,
1289            failure_cause = ?failure_cause,
1290            "persistent driver realized machine-owned failed-run replay"
1291        );
1292        let (checkpoint, input_states, commit) = self
1293            .lifecycle_persistence_payload_with_rollback(checkpoint, "failed-run terminal event")?;
1294        let persist_result = if let Some(applied_commit) = applied_commit {
1295            let session = match serde_json::from_slice::<meerkat_core::Session>(
1296                &applied_commit.session_snapshot,
1297            ) {
1298                Ok(session) => session,
1299                Err(error) => {
1300                    self.inner.restore_rollback_snapshot(checkpoint);
1301                    return Err(RuntimeDriverError::Internal(format!(
1302                        "machine-terminal session snapshot was not a Session: {error}"
1303                    )));
1304                }
1305            };
1306            if session.id() != &applied_commit.owner_session_id {
1307                self.inner.restore_rollback_snapshot(checkpoint);
1308                return Err(RuntimeDriverError::Internal(format!(
1309                    "machine-terminal session owner changed after validation: generated {}, snapshot {}",
1310                    applied_commit.owner_session_id,
1311                    session.id()
1312                )));
1313            }
1314            self.store
1315                .atomic_apply_with_machine_lifecycle(
1316                    &self.runtime_id,
1317                    crate::store::SessionDelta {
1318                        session_snapshot: applied_commit.session_snapshot,
1319                    },
1320                    applied_commit.receipt,
1321                    commit,
1322                    input_states,
1323                    applied_commit.owner_session_id,
1324                )
1325                .await
1326        } else {
1327            self.store
1328                .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1329                .await
1330        };
1331        if let Err(err) = persist_result {
1332            self.inner.restore_rollback_snapshot(checkpoint);
1333            return Err(RuntimeDriverError::Internal(format!(
1334                "terminal event persist failed: {err}"
1335            )));
1336        }
1337        Ok(())
1338    }
1339
1340    pub(crate) async fn machine_realize_run_cancelled(
1341        &mut self,
1342        run_id: &RunId,
1343        contributing_input_ids: &[InputId],
1344    ) -> Result<(), RuntimeDriverError> {
1345        let checkpoint = self.inner.rollback_snapshot();
1346        if let Err(err) = self
1347            .inner
1348            .machine_realize_run_cancelled(run_id, contributing_input_ids)
1349        {
1350            self.inner.restore_rollback_snapshot(checkpoint);
1351            return Err(err);
1352        }
1353        tracing::debug!(
1354            run_id = ?run_id,
1355            contributors = contributing_input_ids.len(),
1356            "persistent driver realized machine-owned cancelled run"
1357        );
1358        let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
1359            checkpoint,
1360            "cancelled-run terminal event",
1361        )?;
1362        if let Err(err) = self
1363            .store
1364            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1365            .await
1366        {
1367            self.inner.restore_rollback_snapshot(checkpoint);
1368            return Err(RuntimeDriverError::Internal(format!(
1369                "terminal cancellation persist failed: {err}"
1370            )));
1371        }
1372        Ok(())
1373    }
1374}
1375
1376#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1377#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1378impl RuntimeDriver for PersistentRuntimeDriver {
1379    async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError> {
1380        let resolved = self.resolve_admission(&input)?;
1381        self.accept_resolved_input(input, resolved).await
1382    }
1383
1384    async fn on_runtime_event(
1385        &mut self,
1386        event: RuntimeEventEnvelope,
1387    ) -> Result<(), RuntimeDriverError> {
1388        self.inner.on_runtime_event(event).await
1389    }
1390
1391    async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError> {
1392        let report = crate::meerkat_machine::machine_recover_persistent_driver(
1393            self.store.as_ref(),
1394            &self.runtime_id,
1395            &mut self.inner,
1396        )
1397        .await?;
1398
1399        let input_states = self.inner.authorized_stored_input_states_snapshot()?;
1400        self.store
1401            .persist_input_states_atomically(&self.runtime_id, &input_states)
1402            .await
1403            .map_err(|err| {
1404                RuntimeDriverError::Internal(format!("recovered input persistence failed: {err}"))
1405            })?;
1406        Ok(report)
1407    }
1408
1409    fn runtime_state(&self) -> RuntimeState {
1410        self.inner.runtime_state()
1411    }
1412
1413    fn input_state(&self, input_id: &InputId) -> Option<&InputState> {
1414        self.inner.input_state(input_id)
1415    }
1416
1417    fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState> {
1418        self.inner.input_phase(input_id)
1419    }
1420
1421    fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId> {
1422        self.inner.input_last_run_id(input_id)
1423    }
1424
1425    fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64> {
1426        self.inner.input_last_boundary_sequence(input_id)
1427    }
1428
1429    fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState> {
1430        self.inner.stored_input_state(input_id)
1431    }
1432
1433    fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
1434        self.inner.stored_input_states_snapshot()
1435    }
1436
1437    fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId> {
1438        self.inner.input_id_for_idempotency_key(idempotency_key)
1439    }
1440
1441    fn active_input_ids(&self) -> Vec<InputId> {
1442        self.inner.active_input_ids()
1443    }
1444}
1445
1446#[cfg(test)]
1447#[allow(clippy::unwrap_used, clippy::expect_used)]
1448mod tests {
1449    use super::*;
1450    use chrono::Utc;
1451    use meerkat_core::lifecycle::InputId;
1452
1453    fn make_prompt(text: &str) -> Input {
1454        Input::Prompt(crate::input::PromptInput {
1455            injected_context: Vec::new(),
1456            header: crate::input::InputHeader {
1457                id: InputId::new(),
1458                timestamp: Utc::now(),
1459                source: crate::input::InputOrigin::Operator,
1460                durability: crate::input::InputDurability::Durable,
1461                visibility: crate::input::InputVisibility::default(),
1462                idempotency_key: None,
1463                supersession_key: None,
1464                correlation_id: None,
1465            },
1466            content: text.into(),
1467            typed_turn_appends: Vec::new(),
1468            turn_metadata: None,
1469        })
1470    }
1471
1472    /// Dogma K11 (Persistent destroy / driver-side shadow truth): every
1473    /// fallible step of `commit_lifecycle_with_rollback` AFTER the caller has
1474    /// staged a DSL lifecycle transition must restore the caller's checkpoint.
1475    /// The input-state snapshot read used to escape with a bare `?`, leaving
1476    /// the staged lifecycle live in driver state while reporting failure.
1477    #[tokio::test]
1478    async fn commit_lifecycle_snapshot_failure_restores_checkpoint() {
1479        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1480        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1481        let rid = LogicalRuntimeId::new("commit-lifecycle-rollback-contract");
1482        let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1483
1484        // Checkpoint BEFORE any state mutation (the caller's pre-stage view).
1485        let checkpoint = driver.rollback_snapshot();
1486
1487        // Mutate driver state past the checkpoint (stands in for a staged
1488        // Destroy/lifecycle transition awaiting durable commit).
1489        let input = make_prompt("staged work");
1490        let input_id = input.id().clone();
1491        let outcome = driver.accept_input(input).await.unwrap();
1492        assert!(outcome.is_accepted());
1493        assert!(driver.input_phase(&input_id).is_some());
1494
1495        // Inject a failure into the input-state snapshot step.
1496        driver.force_input_snapshot_failure_for_test = true;
1497        let target_state = driver.inner_ref().runtime_state();
1498        let result = driver
1499            .commit_lifecycle_with_rollback(checkpoint, target_state, "test destroy")
1500            .await;
1501
1502        // The failure must propagate typed AND the staged driver state must be
1503        // rolled back to the checkpoint — no half-destroyed shadow truth.
1504        assert!(result.is_err(), "forced snapshot failure must propagate");
1505        assert!(
1506            driver.input_phase(&input_id).is_none(),
1507            "staged driver state must be restored to the pre-stage checkpoint"
1508        );
1509        assert!(driver.active_input_ids().is_empty());
1510    }
1511
1512    /// Same K11 checkpoint-restore contract for `abandon_pending_inputs`: the
1513    /// input-state snapshot / lifecycle-commit classification steps between
1514    /// the staged `&mut` abandon and the durable commit used to escape with a
1515    /// bare `?`, leaving the abandon applied in memory while reporting
1516    /// failure (and never persisting it).
1517    #[tokio::test]
1518    async fn abandon_pending_inputs_snapshot_failure_restores_checkpoint() {
1519        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1520        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1521        let rid = LogicalRuntimeId::new("abandon-rollback-contract");
1522        let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
1523
1524        // Accept a pending input so the abandon has staged work to mutate.
1525        let input = make_prompt("pending work");
1526        let input_id = input.id().clone();
1527        let outcome = driver.accept_input(input).await.unwrap();
1528        assert!(outcome.is_accepted());
1529        assert!(driver.input_phase(&input_id).is_some());
1530
1531        // Inject a failure into the input-state snapshot step that runs after
1532        // the staged abandon mutation.
1533        driver.force_input_snapshot_failure_for_test = true;
1534        let result = driver
1535            .abandon_pending_inputs(InputAbandonReason::Reset)
1536            .await;
1537
1538        assert!(result.is_err(), "forced snapshot failure must propagate");
1539        assert!(
1540            driver.input_phase(&input_id).is_some(),
1541            "staged abandon must be rolled back: the pending input must still be live"
1542        );
1543    }
1544
1545    #[tokio::test]
1546    async fn retiring_active_run_persists_retired_before_dropping_live_witness() {
1547        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1548        let runtime_id = LogicalRuntimeId::new("retire-active-run-durability");
1549        let runtime_store: Arc<dyn RuntimeStore> = store.clone();
1550        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1551        let mut driver =
1552            PersistentRuntimeDriver::new(runtime_id.clone(), runtime_store, blob_store);
1553        let run_id = RunId::new();
1554
1555        driver
1556            .contract_begin_run_authority(run_id.clone())
1557            .expect("contract run admission");
1558        assert_eq!(driver.runtime_state(), RuntimeState::Running);
1559        assert_eq!(driver.inner_ref().current_run_id(), Some(run_id));
1560        assert!(driver.inner_ref().pre_run_phase().is_some());
1561
1562        let session_id = driver.inner_ref().session_authority_id_for_recovery();
1563        {
1564            let authority = driver.inner_ref().shared_dsl_authority();
1565            let mut authority = authority
1566                .lock()
1567                .unwrap_or_else(std::sync::PoisonError::into_inner);
1568            crate::meerkat_machine::dsl::MeerkatMachineMutator::apply(
1569                &mut *authority,
1570                crate::meerkat_machine::dsl::MeerkatMachineInput::Retire { session_id },
1571            )
1572            .expect("machine-authorized mid-run retire transition");
1573        }
1574        driver.sync_control_projection_from_dsl_authority();
1575        assert_eq!(driver.runtime_state(), RuntimeState::Retired);
1576        assert!(
1577            driver.inner_ref().pre_run_phase().is_some(),
1578            "Retire commits before the live run witness is dropped"
1579        );
1580
1581        driver
1582            .realize_retire_lifecycle()
1583            .await
1584            .expect("mid-run retire must durably commit");
1585
1586        assert_eq!(driver.runtime_state(), RuntimeState::Retired);
1587        assert_eq!(
1588            crate::store::load_runtime_state(store.as_ref(), &runtime_id)
1589                .await
1590                .expect("reload durable lifecycle"),
1591            Some(RuntimeState::Retired)
1592        );
1593    }
1594
1595    #[tokio::test]
1596    async fn interaction_terminal_outbox_delegator_swaps_exact_rows_and_reports_stale() {
1597        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1598        let store_trait: Arc<dyn RuntimeStore> = store.clone();
1599        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1600        let rid = LogicalRuntimeId::new("interaction-outbox-cas-delegator");
1601        let mut driver = PersistentRuntimeDriver::new(rid.clone(), store_trait, blob_store);
1602
1603        let mut input_ids = Vec::new();
1604        for text in ["first", "second"] {
1605            let input = make_prompt(text);
1606            input_ids.push(input.id().clone());
1607            assert!(driver.accept_input(input).await.unwrap().is_accepted());
1608        }
1609        // The persistent accept path intentionally previews and durably
1610        // commits an isolated staged driver before realizing the same
1611        // admission in the live driver.  Capture the CAS witness from the
1612        // durable store, as recovery adoption does, instead of assuming the
1613        // two independently timestamped admission shells are byte-identical.
1614        let expected = store.load_input_states_strict(&rid).await.unwrap();
1615        for input_id in &input_ids {
1616            driver
1617                .inner_mut()
1618                .ledger_mut()
1619                .get_mut(input_id)
1620                .unwrap()
1621                .recovery_count = 1;
1622        }
1623
1624        assert_eq!(
1625            driver
1626                .compare_and_swap_interaction_terminal_outbox_inputs(&expected, &input_ids)
1627                .await
1628                .unwrap(),
1629            InputStateBatchCasOutcome::Swapped
1630        );
1631        assert!(
1632            store
1633                .load_input_states_strict(&rid)
1634                .await
1635                .unwrap()
1636                .iter()
1637                .all(|row| row.state.recovery_count == 1)
1638        );
1639
1640        for input_id in &input_ids {
1641            driver
1642                .inner_mut()
1643                .ledger_mut()
1644                .get_mut(input_id)
1645                .unwrap()
1646                .recovery_count = 2;
1647        }
1648        assert_eq!(
1649            driver
1650                .compare_and_swap_interaction_terminal_outbox_inputs(&expected, &input_ids)
1651                .await
1652                .unwrap(),
1653            InputStateBatchCasOutcome::Stale
1654        );
1655        assert!(
1656            store
1657                .load_input_states_strict(&rid)
1658                .await
1659                .unwrap()
1660                .iter()
1661                .all(|row| row.state.recovery_count == 1),
1662            "a stale delegator CAS must not mutate any durable row"
1663        );
1664    }
1665
1666    #[tokio::test]
1667    async fn recover_atomically_rewrites_cold_running_lifecycle_to_idle() {
1668        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1669        let runtime_id = LogicalRuntimeId::new("rewrite-cold-running-lifecycle");
1670        store
1671            .commit_machine_lifecycle(
1672                &runtime_id,
1673                MachineLifecycleCommit::new_with_binding(
1674                    RuntimeState::Running,
1675                    crate::store::MachineLifecycleBindingFacts::new(
1676                        Some("rt:cold-running".to_string()),
1677                        Some(9),
1678                        Some(2),
1679                        Some("epoch-cold-running".to_string()),
1680                    ),
1681                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
1682                ),
1683                &[],
1684            )
1685            .await
1686            .expect("seed legacy cold Running lifecycle");
1687
1688        let runtime_store: Arc<dyn RuntimeStore> = store.clone();
1689        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1690        let mut driver =
1691            PersistentRuntimeDriver::new(runtime_id.clone(), runtime_store, blob_store);
1692
1693        driver
1694            .recover()
1695            .await
1696            .expect("cold Running recovery should converge durably");
1697
1698        assert_eq!(driver.runtime_state(), RuntimeState::Idle);
1699        assert_eq!(
1700            crate::store::load_runtime_state(store.as_ref(), &runtime_id)
1701                .await
1702                .expect("reload durable lifecycle"),
1703            Some(RuntimeState::Idle),
1704            "recovery acknowledgement must mean the torn lifecycle row is repaired"
1705        );
1706    }
1707
1708    #[tokio::test]
1709    async fn exact_batch_cas_fences_stale_two_handle_finalization_and_publication_writes() {
1710        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
1711        let store_trait: Arc<dyn RuntimeStore> = store.clone();
1712        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
1713        let rid = LogicalRuntimeId::new("interaction-outbox-two-handle-phase-fence");
1714        let mut owner =
1715            PersistentRuntimeDriver::new(rid.clone(), store_trait.clone(), blob_store.clone());
1716        let mut input_ids = Vec::new();
1717        for text in ["first", "second"] {
1718            let input = make_prompt(text);
1719            input_ids.push(input.id().clone());
1720            assert!(owner.accept_input(input).await.unwrap().is_accepted());
1721        }
1722
1723        // First owner acquires the durable batch witness.
1724        let initial = store.load_input_states_strict(&rid).await.unwrap();
1725        for input_id in &input_ids {
1726            owner
1727                .inner_mut()
1728                .ledger_mut()
1729                .get_mut(input_id)
1730                .unwrap()
1731                .recovery_count = 10;
1732        }
1733        assert_eq!(
1734            owner
1735                .compare_and_swap_interaction_terminal_outbox_inputs(&initial, &input_ids)
1736                .await
1737                .unwrap(),
1738            InputStateBatchCasOutcome::Swapped
1739        );
1740        let owner_witness = store.load_input_states_strict(&rid).await.unwrap();
1741
1742        // A second store handle takes over before Candidate -> Finalized.
1743        let mut takeover =
1744            PersistentRuntimeDriver::new(rid.clone(), store_trait.clone(), blob_store.clone());
1745        RuntimeDriver::recover(&mut takeover).await.unwrap();
1746        let takeover_expected = store.load_input_states_strict(&rid).await.unwrap();
1747        for input_id in &input_ids {
1748            takeover
1749                .inner_mut()
1750                .ledger_mut()
1751                .get_mut(input_id)
1752                .unwrap()
1753                .recovery_count = 20;
1754        }
1755        assert_eq!(
1756            takeover
1757                .compare_and_swap_interaction_terminal_outbox_inputs(
1758                    &takeover_expected,
1759                    &input_ids,
1760                )
1761                .await
1762                .unwrap(),
1763            InputStateBatchCasOutcome::Swapped
1764        );
1765        for input_id in &input_ids {
1766            owner
1767                .inner_mut()
1768                .ledger_mut()
1769                .get_mut(input_id)
1770                .unwrap()
1771                .recovery_count = 30;
1772        }
1773        assert_eq!(
1774            owner
1775                .compare_and_swap_interaction_terminal_outbox_inputs(&owner_witness, &input_ids)
1776                .await
1777                .unwrap(),
1778            InputStateBatchCasOutcome::Stale,
1779            "the superseded owner must not overwrite takeover at finalization"
1780        );
1781        assert!(
1782            store
1783                .load_input_states_strict(&rid)
1784                .await
1785                .unwrap()
1786                .iter()
1787                .all(|row| row.state.recovery_count == 20)
1788        );
1789
1790        // The takeover owner finalizes, then a third handle takes ownership
1791        // before Finalized -> Published. The old finalizer's receipt write is
1792        // fenced by its exact pre-publication witness.
1793        let takeover_witness = store.load_input_states_strict(&rid).await.unwrap();
1794        for input_id in &input_ids {
1795            takeover
1796                .inner_mut()
1797                .ledger_mut()
1798                .get_mut(input_id)
1799                .unwrap()
1800                .recovery_count = 40;
1801        }
1802        assert_eq!(
1803            takeover
1804                .compare_and_swap_interaction_terminal_outbox_inputs(&takeover_witness, &input_ids,)
1805                .await
1806                .unwrap(),
1807            InputStateBatchCasOutcome::Swapped
1808        );
1809        let finalized_witness = store.load_input_states_strict(&rid).await.unwrap();
1810        let mut publisher = PersistentRuntimeDriver::new(rid.clone(), store_trait, blob_store);
1811        RuntimeDriver::recover(&mut publisher).await.unwrap();
1812        let publisher_expected = store.load_input_states_strict(&rid).await.unwrap();
1813        for input_id in &input_ids {
1814            publisher
1815                .inner_mut()
1816                .ledger_mut()
1817                .get_mut(input_id)
1818                .unwrap()
1819                .recovery_count = 50;
1820        }
1821        assert_eq!(
1822            publisher
1823                .compare_and_swap_interaction_terminal_outbox_inputs(
1824                    &publisher_expected,
1825                    &input_ids,
1826                )
1827                .await
1828                .unwrap(),
1829            InputStateBatchCasOutcome::Swapped
1830        );
1831        for input_id in &input_ids {
1832            takeover
1833                .inner_mut()
1834                .ledger_mut()
1835                .get_mut(input_id)
1836                .unwrap()
1837                .recovery_count = 60;
1838        }
1839        assert_eq!(
1840            takeover
1841                .compare_and_swap_interaction_terminal_outbox_inputs(
1842                    &finalized_witness,
1843                    &input_ids,
1844                )
1845                .await
1846                .unwrap(),
1847            InputStateBatchCasOutcome::Stale,
1848            "the superseded finalizer must not overwrite takeover at publication"
1849        );
1850        assert!(
1851            store
1852                .load_input_states_strict(&rid)
1853                .await
1854                .unwrap()
1855                .iter()
1856                .all(|row| row.state.recovery_count == 50)
1857        );
1858    }
1859}