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