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::core_executor::BoundSessionCommit;
12use meerkat_core::lifecycle::{InputId, RunBoundaryReceipt, RunId};
13
14use crate::accept::AcceptOutcome;
15use crate::identifiers::LogicalRuntimeId;
16use crate::input::{Input, externalize_input_images};
17use crate::input_state::{
18    InputAbandonReason, InputLifecycleState, InputState, InputStatePersistenceRecord,
19    InputStateSeed, StoredInputState,
20};
21use crate::runtime_event::RuntimeEventEnvelope;
22use crate::runtime_state::RuntimeState;
23use crate::store::{
24    FencedInputStateBatchCasOutcome, InputStateBatchCasImplementationProfile,
25    InputStateBatchCasOutcome, MachineLifecycleCommit, PreparedHeadCanonicalProvisionalPromotion,
26    PreparedRuntimeSessionCommit, PreparedRuntimeSessionCommitResult,
27    PreparedWholeBlobProvisionalPromotion, RecoveryInputStateMutation,
28    RuntimeSessionPersistenceProfile, RuntimeStore, RuntimeStoreError, RuntimeStoreWriteFence,
29};
30use crate::traits::{DestroyReport, RecoveryReport, RuntimeDriver, RuntimeDriverError};
31
32use super::ephemeral::{
33    EphemeralDriverRollbackSnapshot, EphemeralRuntimeDriver, SharedIngressDslAuthority,
34};
35
36/// Persistent runtime driver — durable InputState via RuntimeStore.
37pub struct PersistentRuntimeDriver {
38    /// Underlying ephemeral driver for state machine logic.
39    inner: EphemeralRuntimeDriver,
40    /// Durable store for InputState + receipts.
41    store: Arc<dyn RuntimeStore>,
42    /// Blob store used to externalize durable input payloads.
43    blob_store: Arc<dyn BlobStore>,
44    /// Runtime ID for store operations.
45    runtime_id: LogicalRuntimeId,
46    /// Shared session-entry durability gate. Production registration always
47    /// supplies this handle; direct constructor users retain compatibility
48    /// rollback behavior but cannot participate in fail-stop rehydration.
49    durability_health: Option<crate::meerkat_machine::DurabilityHealthHandle>,
50    /// Exact durable writer epoch retained from conditional registration.
51    ///
52    /// Multi-writer stores never consume this capability. An
53    /// `ExclusiveWriterFenced` store must validate this same guard inside each
54    /// complete exact-batch write.
55    input_state_write_fence: Option<Arc<dyn RuntimeStoreWriteFence>>,
56    /// Test-only fault injection: forces the input-state snapshot step of
57    /// [`Self::commit_lifecycle_with_rollback`] to fail so tests can pin the
58    /// checkpoint-restore contract for that arm.
59    #[cfg(test)]
60    pub(crate) force_input_snapshot_failure_for_test: bool,
61}
62
63enum PreparedProvisionalPromotion {
64    WholeBlob(PreparedWholeBlobProvisionalPromotion),
65    HeadCanonical(PreparedHeadCanonicalProvisionalPromotion),
66}
67
68impl PersistentRuntimeDriver {
69    fn prepare_provisional_promotion(
70        &self,
71        checkpoint: &meerkat_core::RunCheckpointReceipt,
72        receipt: &RunBoundaryReceipt,
73        owner_session_id: &meerkat_core::types::SessionId,
74    ) -> Result<PreparedProvisionalPromotion, RuntimeStoreError> {
75        if checkpoint.session_id() != owner_session_id {
76            return Err(RuntimeStoreError::SessionKeyMismatch {
77                expected: checkpoint.session_id().clone(),
78                actual: owner_session_id.clone(),
79            });
80        }
81        if checkpoint.run_id() != &receipt.run_id {
82            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
83                runtime_id: self.runtime_id.to_string(),
84                detail: "provisional promotion receipt run differs from terminal boundary run"
85                    .to_string(),
86            });
87        }
88        match self.store.session_persistence_profile() {
89            RuntimeSessionPersistenceProfile::WholeBlobV1 if checkpoint.whole_blob().is_some() => {
90                PreparedWholeBlobProvisionalPromotion::prepare(checkpoint.clone(), &receipt.run_id)
91                    .map(PreparedProvisionalPromotion::WholeBlob)
92            }
93            RuntimeSessionPersistenceProfile::HeadCanonicalV1
94                if checkpoint.head_canonical().is_some() =>
95            {
96                PreparedHeadCanonicalProvisionalPromotion::prepare(
97                    checkpoint.clone(),
98                    &receipt.run_id,
99                )
100                .map(PreparedProvisionalPromotion::HeadCanonical)
101            }
102            profile => Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
103                runtime_id: self.runtime_id.to_string(),
104                detail: format!(
105                    "provisional promotion receipt profile {checkpoint:?} cannot commit through {profile}"
106                ),
107            }),
108        }
109    }
110
111    fn prepare_success_boundary(
112        &self,
113        session: Option<BoundSessionCommit>,
114        receipt: RunBoundaryReceipt,
115        input_updates: Vec<InputStatePersistenceRecord>,
116        owner_session_id: meerkat_core::types::SessionId,
117    ) -> Result<PreparedRuntimeSessionCommit, RuntimeStoreError> {
118        let Some(session) = session else {
119            return Ok(PreparedRuntimeSessionCommit::success(
120                None,
121                receipt,
122                input_updates,
123                Some(owner_session_id),
124            ));
125        };
126        let Some(checkpoint_receipt) = session.provisional_promotion_receipt().cloned() else {
127            return Ok(PreparedRuntimeSessionCommit::success(
128                Some(session),
129                receipt,
130                input_updates,
131                Some(owner_session_id),
132            ));
133        };
134        match self.prepare_provisional_promotion(
135            &checkpoint_receipt,
136            &receipt,
137            &owner_session_id,
138        )? {
139            PreparedProvisionalPromotion::WholeBlob(promotion) => {
140                PreparedRuntimeSessionCommit::promote_whole_blob_success(
141                    promotion,
142                    receipt,
143                    input_updates,
144                    owner_session_id,
145                )
146            }
147            PreparedProvisionalPromotion::HeadCanonical(promotion) => {
148                PreparedRuntimeSessionCommit::promote_head_canonical_success(
149                    promotion,
150                    receipt,
151                    input_updates,
152                    owner_session_id,
153                )
154            }
155        }
156    }
157
158    fn prepare_machine_terminal_boundary(
159        &self,
160        session: BoundSessionCommit,
161        receipt: RunBoundaryReceipt,
162        machine_lifecycle: MachineLifecycleCommit,
163        input_updates: Vec<InputStatePersistenceRecord>,
164        owner_session_id: meerkat_core::types::SessionId,
165    ) -> Result<PreparedRuntimeSessionCommit, RuntimeStoreError> {
166        let Some(checkpoint_receipt) = session.provisional_promotion_receipt().cloned() else {
167            return Ok(PreparedRuntimeSessionCommit::machine_terminal(
168                session,
169                receipt,
170                machine_lifecycle,
171                input_updates,
172                owner_session_id,
173            ));
174        };
175        match self.prepare_provisional_promotion(
176            &checkpoint_receipt,
177            &receipt,
178            &owner_session_id,
179        )? {
180            PreparedProvisionalPromotion::WholeBlob(promotion) => {
181                PreparedRuntimeSessionCommit::promote_whole_blob_machine_terminal(
182                    promotion,
183                    receipt,
184                    machine_lifecycle,
185                    input_updates,
186                    owner_session_id,
187                )
188            }
189            PreparedProvisionalPromotion::HeadCanonical(promotion) => {
190                PreparedRuntimeSessionCommit::promote_head_canonical_machine_terminal(
191                    promotion,
192                    receipt,
193                    machine_lifecycle,
194                    input_updates,
195                    owner_session_id,
196                )
197            }
198        }
199    }
200
201    async fn recover_and_prepare_input_mutations(
202        &mut self,
203        recovered_unregister_progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
204    ) -> Result<
205        (
206            RecoveryReport,
207            crate::store::RecoveryInputSetRevision,
208            Vec<RecoveryInputStateMutation>,
209        ),
210        RuntimeDriverError,
211    > {
212        let snapshot = self
213            .store
214            .load_input_states_with_versions(&self.runtime_id)
215            .await
216            .map_err(|error| match error {
217                crate::store::RuntimeStoreError::Unsupported(reason) => {
218                    RuntimeDriverError::RecoveryRepairBlocked {
219                        evidence_digest: None,
220                        reason: format!(
221                            "runtime store cannot produce an exact recovery input-set witness: \
222                             {reason}"
223                        ),
224                    }
225                }
226                other => RuntimeDriverError::RecoveryBackoff {
227                    reason: format!("failed to observe durable inputs for recovery: {other}"),
228                },
229            })?;
230        if snapshot.runtime_id() != &self.runtime_id {
231            return Err(RuntimeDriverError::RecoveryCorruption {
232                reason: format!(
233                    "runtime store prepared recovery input-set evidence for `{}` while \
234                     recovering `{}`",
235                    snapshot.runtime_id(),
236                    self.runtime_id
237                ),
238            });
239        }
240        let (rows, input_set_revision, exact_set_token) = snapshot.into_parts();
241        let mut observed = Vec::with_capacity(rows.len());
242        let mut exact_observations = Vec::with_capacity(rows.len());
243        for (bundle, row_digest) in rows {
244            let disposition =
245                crate::meerkat_machine::driver::machine_classify_recovered_input_durability(
246                    &bundle.state,
247                )?;
248            exact_observations.push((bundle.state.input_id.clone(), row_digest, disposition));
249            observed.push(bundle);
250        }
251        // Terminal rows are outside the recovery nonterminal set by design,
252        // but unfinished completion/publication carriers must still be
253        // rehydrated so their exact durable saga can converge after restart.
254        // The store-owned input-set revision advances for every input-row
255        // mutation, including these terminal rows, so the final recovery CAS
256        // still fences this second indexed observation without hashing or
257        // rescanning historical terminal rows.
258        let pending_terminal = self.durable_pending_terminal_input_states().await?;
259        let mut observed_ids = observed
260            .iter()
261            .map(|stored| stored.state.input_id.clone())
262            .collect::<std::collections::HashSet<_>>();
263        for stored in pending_terminal {
264            if !observed_ids.insert(stored.state.input_id.clone()) {
265                return Err(RuntimeDriverError::RecoveryCorruption {
266                    reason: format!(
267                        "input {} appeared in both nonterminal recovery and pending-terminal \
268                         observations",
269                        stored.state.input_id
270                    ),
271                });
272            }
273            observed.push(stored);
274        }
275
276        let report = crate::meerkat_machine::machine_recover_persistent_inputs_from_observed(
277            self.store.as_ref(),
278            &self.runtime_id,
279            &mut self.inner,
280            observed,
281            recovered_unregister_progress,
282        )
283        .await?;
284
285        let mut mutations = Vec::with_capacity(exact_observations.len());
286        for (input_id, row_digest, disposition) in exact_observations {
287            if matches!(
288                disposition,
289                crate::meerkat_machine::dsl::RecoveredInputRecoveryDisposition::Discard
290            ) {
291                mutations.push(
292                    RecoveryInputStateMutation::delete(input_id, row_digest).map_err(|error| {
293                        RuntimeDriverError::RecoveryCorruption {
294                            reason: format!(
295                                "machine-authorized recovery delete lost its exact row witness: \
296                                 {error}"
297                            ),
298                        }
299                    })?,
300                );
301                continue;
302            }
303
304            let record = self
305                .inner
306                .authorized_stored_input_state(&input_id)?
307                .ok_or_else(|| RuntimeDriverError::RecoveryCorruption {
308                    reason: format!(
309                        "recovered durable input {input_id} is absent from machine authority"
310                    ),
311                })?
312                .with_expected_row_digest(row_digest);
313            mutations.push(RecoveryInputStateMutation::Upsert(record));
314        }
315        tracing::debug!(
316            runtime_id = %self.runtime_id,
317            recovery_input_set_token = %exact_set_token,
318            recovery_input_mutations = mutations.len(),
319            "prepared exact revision-fenced cold input recovery"
320        );
321        Ok((report, input_set_revision, mutations))
322    }
323
324    pub(crate) async fn recover_inputs_after_runtime_authority(
325        &mut self,
326        recovered_unregister_progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
327    ) -> Result<RecoveryReport, RuntimeDriverError> {
328        match self.store.input_state_batch_cas_implementation_profile() {
329            InputStateBatchCasImplementationProfile::MultiWriter => {}
330            InputStateBatchCasImplementationProfile::ExclusiveWriterFenced => {
331                return Err(RuntimeDriverError::RecoveryRepairBlocked {
332                    evidence_digest: None,
333                    reason: "exclusive-writer input recovery requires conditional registration \
334                             with a durable write fence"
335                        .to_string(),
336                });
337            }
338            InputStateBatchCasImplementationProfile::Unsupported => {
339                return Err(RuntimeDriverError::RecoveryRepairBlocked {
340                    evidence_digest: None,
341                    reason: "runtime store does not implement exact input-state batch CAS"
342                        .to_string(),
343                });
344            }
345        }
346
347        let (report, input_set_revision, mutations) = self
348            .recover_and_prepare_input_mutations(recovered_unregister_progress)
349            .await?;
350
351        match self
352            .store
353            .compare_and_swap_recovery_input_states_atomically(
354                &self.runtime_id,
355                input_set_revision,
356                &mutations,
357            )
358            .await
359            .map_err(|err| RuntimeDriverError::RecoveryBackoff {
360                reason: format!("recovered input exact-batch CAS failed: {err}"),
361            })? {
362            InputStateBatchCasOutcome::Swapped => Ok(report),
363            InputStateBatchCasOutcome::Stale => Err(RuntimeDriverError::RecoveryBackoff {
364                reason: "durable input state changed while cold recovery was preparing".to_string(),
365            }),
366        }
367    }
368
369    /// Recover durable input work and publish the normalized target image only
370    /// while both the original input rows and the caller's external authority
371    /// fence remain current.
372    pub(crate) async fn recover_inputs_after_runtime_authority_with_fence(
373        &mut self,
374        recovered_unregister_progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
375        write_fence: Arc<dyn RuntimeStoreWriteFence>,
376    ) -> Result<RecoveryReport, RuntimeDriverError> {
377        let (report, input_set_revision, mutations) = self
378            .recover_and_prepare_input_mutations(recovered_unregister_progress)
379            .await?;
380
381        match self.store.input_state_batch_cas_implementation_profile() {
382            InputStateBatchCasImplementationProfile::MultiWriter => {
383                match self
384                    .store
385                    .compare_and_swap_recovery_input_states_atomically(
386                        &self.runtime_id,
387                        input_set_revision,
388                        &mutations,
389                    )
390                    .await
391                    .map_err(|error| RuntimeDriverError::RecoveryBackoff {
392                        reason: format!("recovered input exact-batch CAS failed: {error}"),
393                    })? {
394                    InputStateBatchCasOutcome::Swapped => Ok(report),
395                    InputStateBatchCasOutcome::Stale => Err(RuntimeDriverError::RecoveryBackoff {
396                        reason: "durable input state changed while cold recovery was preparing"
397                            .to_string(),
398                    }),
399                }
400            }
401            InputStateBatchCasImplementationProfile::ExclusiveWriterFenced => {
402                match self
403                    .store
404                    .compare_and_swap_recovery_input_states_atomically_with_fence(
405                        &self.runtime_id,
406                        input_set_revision,
407                        &mutations,
408                        write_fence,
409                    )
410                    .await
411                    .map_err(|error| match error {
412                        crate::store::RuntimeStoreError::Unsupported(reason) => {
413                            RuntimeDriverError::RecoveryRepairBlocked {
414                                evidence_digest: None,
415                                reason: format!(
416                                    "runtime store lacks fenced input recovery capability: {reason}"
417                                ),
418                            }
419                        }
420                        other => RuntimeDriverError::RecoveryBackoff {
421                            reason: format!("fenced recovered input persistence failed: {other}"),
422                        },
423                    })? {
424                    FencedInputStateBatchCasOutcome::Swapped => Ok(report),
425                    FencedInputStateBatchCasOutcome::Stale => {
426                        Err(RuntimeDriverError::StaleAuthority {
427                            reason: "durable input state changed while cold recovery was preparing"
428                                .to_string(),
429                        })
430                    }
431                    FencedInputStateBatchCasOutcome::FenceConflict { reason } => {
432                        Err(RuntimeDriverError::StaleAuthority { reason })
433                    }
434                    FencedInputStateBatchCasOutcome::FenceBackoff { reason } => {
435                        Err(RuntimeDriverError::RecoveryBackoff { reason })
436                    }
437                }
438            }
439            InputStateBatchCasImplementationProfile::Unsupported => {
440                Err(RuntimeDriverError::RecoveryRepairBlocked {
441                    evidence_digest: None,
442                    reason: "runtime store does not implement exact input-state batch CAS"
443                        .to_string(),
444                })
445            }
446        }
447    }
448
449    /// Create a new persistent runtime driver.
450    pub fn new(
451        runtime_id: LogicalRuntimeId,
452        store: Arc<dyn RuntimeStore>,
453        blob_store: Arc<dyn BlobStore>,
454    ) -> Self {
455        Self::new_with_control(
456            runtime_id,
457            store,
458            blob_store,
459            Arc::new(StdRwLock::new(
460                crate::driver::ephemeral::RuntimeControlProjection::default(),
461            )),
462            crate::driver::ephemeral::new_ingress_dsl_authority(),
463        )
464    }
465
466    pub(crate) fn new_with_control(
467        runtime_id: LogicalRuntimeId,
468        store: Arc<dyn RuntimeStore>,
469        blob_store: Arc<dyn BlobStore>,
470        control: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
471        dsl: SharedIngressDslAuthority,
472    ) -> Self {
473        Self {
474            inner: EphemeralRuntimeDriver::new_with_control_and_dsl(
475                runtime_id.clone(),
476                control,
477                dsl,
478            ),
479            store,
480            blob_store,
481            runtime_id,
482            durability_health: None,
483            input_state_write_fence: None,
484            #[cfg(test)]
485            force_input_snapshot_failure_for_test: false,
486        }
487    }
488
489    pub(crate) fn new_with_control_and_durability_health(
490        runtime_id: LogicalRuntimeId,
491        store: Arc<dyn RuntimeStore>,
492        blob_store: Arc<dyn BlobStore>,
493        control: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
494        dsl: SharedIngressDslAuthority,
495        durability_health: crate::meerkat_machine::DurabilityHealthHandle,
496    ) -> Self {
497        Self {
498            inner: EphemeralRuntimeDriver::new_with_control_and_dsl(
499                runtime_id.clone(),
500                control,
501                dsl,
502            ),
503            store,
504            blob_store,
505            runtime_id,
506            durability_health: Some(durability_health),
507            input_state_write_fence: None,
508            #[cfg(test)]
509            force_input_snapshot_failure_for_test: false,
510        }
511    }
512
513    pub(crate) fn set_input_state_write_fence(
514        &mut self,
515        write_fence: Arc<dyn RuntimeStoreWriteFence>,
516    ) {
517        self.input_state_write_fence = Some(write_fence);
518    }
519
520    pub(crate) fn require_durability_ready(&self) -> Result<(), RuntimeDriverError> {
521        match self.durability_health.as_ref() {
522            Some(health) => health.require_ready().map_err(|required| {
523                RuntimeDriverError::RecoveryRepairBlocked {
524                    evidence_digest: None,
525                    reason: required.to_string(),
526                }
527            }),
528            None => Ok(()),
529        }
530    }
531
532    /// Clone the shared fail-closed handle for a cancellation guard that must
533    /// outlive a borrow of this driver across an async durable commit.
534    pub(crate) fn durability_health_handle(
535        &self,
536    ) -> Option<crate::meerkat_machine::DurabilityHealthHandle> {
537        self.durability_health.clone()
538    }
539
540    /// Degrade this production persistent shell after a transition or durable
541    /// commit can no longer be reconciled in place. The shared session gate
542    /// retains the first failure and refuses every later ordinary mutation
543    /// until registration cold-loads a fresh driver.
544    pub(crate) fn mark_durability_reload_required(
545        &self,
546        operation: &'static str,
547        reason: impl Into<String>,
548    ) -> RuntimeDriverError {
549        let reason = reason.into();
550        if let Some(health) = self.durability_health.as_ref() {
551            health.mark_reload_required(operation, reason.clone());
552            RuntimeDriverError::RecoveryRepairBlocked {
553                evidence_digest: None,
554                reason: format!(
555                    "durable state may differ from the live runtime after `{operation}`; \
556                     registration-authorized cold reload is required: {reason}"
557                ),
558            }
559        } else {
560            RuntimeDriverError::Internal(reason)
561        }
562    }
563
564    fn persistence_rollback_checkpoint(&self) -> Option<EphemeralDriverRollbackSnapshot> {
565        self.durability_health
566            .is_none()
567            .then(|| self.inner.rollback_snapshot())
568    }
569
570    fn restore_compatibility_checkpoint(
571        &mut self,
572        checkpoint: Option<EphemeralDriverRollbackSnapshot>,
573    ) {
574        if let Some(checkpoint) = checkpoint {
575            self.inner.restore_rollback_snapshot(checkpoint);
576        }
577    }
578
579    fn post_transition_failure(
580        &mut self,
581        checkpoint: Option<EphemeralDriverRollbackSnapshot>,
582        operation: &'static str,
583        reason: impl Into<String>,
584    ) -> RuntimeDriverError {
585        let reason = reason.into();
586        if self.durability_health.is_some() {
587            self.mark_durability_reload_required(operation, reason)
588        } else {
589            self.restore_compatibility_checkpoint(checkpoint);
590            RuntimeDriverError::Internal(reason)
591        }
592    }
593
594    pub(crate) fn input_state_batch_cas_implementation_profile(
595        &self,
596    ) -> InputStateBatchCasImplementationProfile {
597        self.store.input_state_batch_cas_implementation_profile()
598    }
599
600    pub(crate) fn input_state_write_fence(&self) -> Option<Arc<dyn RuntimeStoreWriteFence>> {
601        self.input_state_write_fence.clone()
602    }
603
604    async fn durable_idempotency_duplicate(
605        &self,
606        input: &Input,
607    ) -> Result<Option<(InputId, InputStateSeed)>, RuntimeDriverError> {
608        let Some(key) = input.header().idempotency_key.as_ref() else {
609            return Ok(None);
610        };
611        let observation = self
612            .store
613            .load_input_state_by_idempotency_key(&self.runtime_id, key)
614            .await
615            .map_err(|error| match error {
616                crate::store::RuntimeStoreError::Unsupported(reason) => {
617                    RuntimeDriverError::RecoveryRepairBlocked {
618                        evidence_digest: None,
619                        reason: format!(
620                            "persistent idempotency admission requires the exact store-owned \
621                             index: {reason}"
622                        ),
623                    }
624                }
625                error @ crate::store::RuntimeStoreError::InputIdempotencyIndexUncertain {
626                    ..
627                } => RuntimeDriverError::RecoveryRepairBlocked {
628                    evidence_digest: None,
629                    reason: format!(
630                        "persistent idempotency admission found durable index corruption: {error}"
631                    ),
632                },
633                other => RuntimeDriverError::Internal(format!(
634                    "persistent idempotency admission lookup failed: {other}"
635                )),
636            })?;
637        let Some(observation) = observation else {
638            return Ok(None);
639        };
640        let (stored, _exact_row_digest) = observation.into_parts();
641        if stored.state.idempotency_key.as_ref() != Some(key) {
642            return Err(RuntimeDriverError::RecoveryCorruption {
643                reason: format!(
644                    "store idempotency index for key `{key}` returned input {} with a different \
645                     key",
646                    stored.state.input_id
647                ),
648            });
649        }
650        Ok(Some((stored.state.input_id, stored.seed)))
651    }
652
653    /// Get immutable reference to the inner ephemeral driver.
654    pub fn inner_ref(&self) -> &EphemeralRuntimeDriver {
655        &self.inner
656    }
657
658    pub(crate) fn inner_mut(&mut self) -> &mut EphemeralRuntimeDriver {
659        &mut self.inner
660    }
661
662    #[cfg(test)]
663    pub(crate) async fn compare_and_swap_interaction_terminal_outbox_inputs(
664        &self,
665        expected: &[StoredInputState],
666        input_ids: &[InputId],
667    ) -> Result<InputStateBatchCasOutcome, RuntimeDriverError> {
668        let mut replacements = Vec::with_capacity(input_ids.len());
669        for input_id in input_ids {
670            let replacement = self
671                .inner
672                .authorized_stored_input_state(input_id)?
673                .ok_or_else(|| {
674                    RuntimeDriverError::Internal(format!(
675                        "interaction terminal outbox input {input_id} disappeared before compare-and-swap"
676                    ))
677                })?;
678            replacements.push(replacement);
679        }
680        self.compare_and_swap_interaction_terminal_outbox_replacements(expected, &replacements)
681            .await
682    }
683
684    pub(crate) async fn compare_and_swap_interaction_terminal_outbox_replacements(
685        &self,
686        expected: &[StoredInputState],
687        replacements: &[crate::input_state::InputStatePersistenceRecord],
688    ) -> Result<InputStateBatchCasOutcome, RuntimeDriverError> {
689        self.require_durability_ready()?;
690        match self.store.input_state_batch_cas_implementation_profile() {
691            InputStateBatchCasImplementationProfile::MultiWriter => self
692                .store
693                .compare_and_swap_input_states_atomically(&self.runtime_id, expected, replacements)
694                .await
695                .map_err(|error| {
696                    self.mark_durability_reload_required(
697                        "interaction_terminal_batch_cas",
698                        format!(
699                            "multi-writer input-state batch compare-and-swap outcome is unknown: \
700                             {error}"
701                        ),
702                    )
703                }),
704            InputStateBatchCasImplementationProfile::ExclusiveWriterFenced => {
705                let write_fence = self.input_state_write_fence.clone().ok_or_else(|| {
706                    self.mark_durability_reload_required(
707                        "interaction_terminal_batch_cas_fence",
708                        "exclusive-writer input-state CAS has no durable registration fence",
709                    )
710                })?;
711                match self
712                    .store
713                    .compare_and_swap_input_states_atomically_with_fence(
714                        &self.runtime_id,
715                        expected,
716                        replacements,
717                        write_fence,
718                    )
719                    .await
720                    .map_err(|error| {
721                        self.mark_durability_reload_required(
722                            "interaction_terminal_fenced_batch_cas",
723                            format!(
724                                "fenced input-state batch compare-and-swap outcome is unknown: \
725                                 {error}"
726                            ),
727                        )
728                    })? {
729                    FencedInputStateBatchCasOutcome::Swapped => {
730                        Ok(InputStateBatchCasOutcome::Swapped)
731                    }
732                    FencedInputStateBatchCasOutcome::Stale => Ok(InputStateBatchCasOutcome::Stale),
733                    FencedInputStateBatchCasOutcome::FenceConflict { reason } => Err(self
734                        .mark_durability_reload_required(
735                            "interaction_terminal_batch_cas_fence_conflict",
736                            reason,
737                        )),
738                    FencedInputStateBatchCasOutcome::FenceBackoff { reason } => {
739                        Err(RuntimeDriverError::RecoveryBackoff { reason })
740                    }
741                }
742            }
743            InputStateBatchCasImplementationProfile::Unsupported => {
744                Err(RuntimeDriverError::RecoveryRepairBlocked {
745                    evidence_digest: None,
746                    reason: "runtime store does not implement exact input-state batch CAS"
747                        .to_string(),
748                })
749            }
750        }
751    }
752
753    /// Release terminal live state after its exact completion/publication CAS
754    /// has committed. The ephemeral helper rechecks that every named row is
755    /// terminal and carries no open durable obligation; any archive mismatch
756    /// degrades the shared shell rather than continuing with split authority.
757    pub(crate) fn archive_terminal_inputs_after_durable_obligations(
758        &mut self,
759        input_ids: &[InputId],
760    ) -> Result<(), RuntimeDriverError> {
761        self.require_durability_ready()?;
762        let archivable = match self.inner.archivable_terminal_input_ids_in(input_ids) {
763            Ok(archivable) if archivable.len() == input_ids.len() => archivable,
764            Ok(archivable) => {
765                return Err(self.post_transition_failure(
766                    None,
767                    "terminal_obligation_archive_classification",
768                    format!(
769                        "only {} of {} exact terminal-obligation inputs were durably quiescent",
770                        archivable.len(),
771                        input_ids.len()
772                    ),
773                ));
774            }
775            Err(error) => {
776                return Err(self.post_transition_failure(
777                    None,
778                    "terminal_obligation_archive_classification",
779                    error.to_string(),
780                ));
781            }
782        };
783        if let Err(error) = self
784            .inner
785            .archive_archivable_terminal_inputs_after_durable_commit(&archivable)
786        {
787            return Err(self.post_transition_failure(
788                None,
789                "terminal_obligation_archive",
790                error.to_string(),
791            ));
792        }
793        Ok(())
794    }
795
796    pub(crate) async fn committed_session_snapshot_for_terminal_recovery(
797        &self,
798    ) -> Result<Option<Arc<Vec<u8>>>, RuntimeDriverError> {
799        self.store
800            .load_session_snapshot(&self.runtime_id)
801            .await
802            .map_err(|error| {
803                RuntimeDriverError::Internal(format!(
804                    "interaction terminal recovery failed to load committed session snapshot: {error}"
805                ))
806            })
807    }
808
809    pub(crate) async fn pending_terminal_owner_ids(
810        &self,
811    ) -> Result<Vec<InputId>, RuntimeDriverError> {
812        let mut owners = Vec::new();
813        let mut after = None;
814        loop {
815            let page = self
816                .store
817                .load_pending_terminal_owner_ids_page(
818                    &self.runtime_id,
819                    after.as_ref(),
820                    crate::store::MAX_PENDING_TERMINAL_OWNER_PAGE,
821                )
822                .await
823                .map_err(|error| match error {
824                    crate::store::RuntimeStoreError::Unsupported(reason) => {
825                        RuntimeDriverError::RecoveryRepairBlocked {
826                            evidence_digest: None,
827                            reason: format!(
828                                "runtime store cannot discover pending terminal owners: {reason}"
829                            ),
830                        }
831                    }
832                    other => RuntimeDriverError::Internal(format!(
833                        "pending terminal owner discovery failed: {other}"
834                    )),
835                })?;
836            crate::store::validate_pending_terminal_owner_page(
837                after.as_ref(),
838                crate::store::MAX_PENDING_TERMINAL_OWNER_PAGE,
839                &page,
840            )
841            .map_err(|error| RuntimeDriverError::RecoveryCorruption {
842                reason: error.to_string(),
843            })?;
844            let short = page.len() < crate::store::MAX_PENDING_TERMINAL_OWNER_PAGE;
845            after = page.last().cloned();
846            owners.extend(page);
847            if short {
848                return Ok(owners);
849            }
850        }
851    }
852
853    pub(crate) async fn durable_pending_terminal_input_states(
854        &self,
855    ) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
856        let owners = self.pending_terminal_owner_ids().await?;
857        let mut rows = std::collections::HashMap::<InputId, StoredInputState>::new();
858        for owner_input_id in owners {
859            let mut owner_rows = self
860                .store
861                .load_input_states_by_ids(&self.runtime_id, std::slice::from_ref(&owner_input_id))
862                .await
863                .map_err(|error| {
864                    RuntimeDriverError::Internal(format!(
865                        "pending terminal owner row read failed: {error}"
866                    ))
867                })?;
868            let owner = owner_rows
869                .pop()
870                .ok_or_else(|| RuntimeDriverError::RecoveryCorruption {
871                    reason: "pending terminal owner read returned the wrong cardinality"
872                        .to_string(),
873                })?
874                .ok_or_else(|| RuntimeDriverError::RecoveryCorruption {
875                    reason: format!(
876                        "pending terminal owner index points to missing input {owner_input_id}"
877                    ),
878                })?;
879            if !crate::store::input_state_is_pending_terminal_owner(&owner.state) {
880                return Err(RuntimeDriverError::RecoveryCorruption {
881                    reason: format!(
882                        "pending terminal owner index points to non-owner input {owner_input_id}"
883                    ),
884                });
885            }
886
887            let mut recipient_ids = Vec::new();
888            if let Some(completion) = owner.state.terminal_completion.as_ref()
889                && completion.owner_input_id == owner_input_id
890                && matches!(
891                    &completion.phase,
892                    crate::input_state::InputTerminalCompletionPhase::Pending
893                )
894            {
895                recipient_ids.extend(
896                    completion
897                        .completion_input_ids
898                        .as_ref()
899                        .ok_or_else(|| RuntimeDriverError::RecoveryCorruption {
900                            reason: format!(
901                                "pending terminal completion owner {owner_input_id} lost recipients"
902                            ),
903                        })?
904                        .iter()
905                        .cloned(),
906                );
907            }
908            if let Some(outbox) = owner.state.interaction_terminal_outbox.as_ref()
909                && outbox.candidate_owner_input_id == owner_input_id
910                && !matches!(
911                    &outbox.phase,
912                    crate::input_state::InteractionTerminalOutboxPhase::Published { .. }
913                )
914            {
915                recipient_ids.extend(
916                    outbox
917                        .completion_input_ids
918                        .as_ref()
919                        .ok_or_else(|| RuntimeDriverError::RecoveryCorruption {
920                            reason: format!(
921                                "pending interaction terminal owner {owner_input_id} lost recipients"
922                            ),
923                        })?
924                        .iter()
925                        .cloned(),
926                );
927            }
928            recipient_ids.sort_by_key(|input_id| input_id.0);
929            recipient_ids.dedup();
930            if recipient_ids.is_empty()
931                || recipient_ids.len() > crate::store::MAX_INPUT_STATE_BATCH_CAS
932            {
933                return Err(RuntimeDriverError::RecoveryCorruption {
934                    reason: format!(
935                        "pending terminal owner {owner_input_id} declares an invalid recipient set"
936                    ),
937                });
938            }
939            let recipient_rows = self
940                .store
941                .load_input_states_by_ids(&self.runtime_id, &recipient_ids)
942                .await
943                .map_err(|error| {
944                    RuntimeDriverError::Internal(format!(
945                        "pending terminal recipient batch read failed: {error}"
946                    ))
947                })?;
948            if recipient_rows.len() != recipient_ids.len() {
949                return Err(RuntimeDriverError::RecoveryCorruption {
950                    reason: "pending terminal recipient read returned the wrong cardinality"
951                        .to_string(),
952                });
953            }
954            for (input_id, row) in recipient_ids.into_iter().zip(recipient_rows) {
955                let row = row.ok_or_else(|| RuntimeDriverError::RecoveryCorruption {
956                    reason: format!(
957                        "pending terminal owner {owner_input_id} points to missing recipient {input_id}"
958                    ),
959                })?;
960                rows.insert(input_id, row);
961            }
962        }
963        let mut rows = rows.into_values().collect::<Vec<_>>();
964        rows.sort_by_key(|row| row.state.input_id.0);
965        Ok(rows)
966    }
967
968    /// Get the logical runtime ID for this driver.
969    pub fn runtime_id(&self) -> &LogicalRuntimeId {
970        &self.runtime_id
971    }
972
973    pub(crate) fn session_persistence_profile(
974        &self,
975    ) -> crate::store::RuntimeSessionPersistenceProfile {
976        self.store.session_persistence_profile()
977    }
978
979    pub(crate) async fn load_pending_compaction_projections(
980        &self,
981    ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeDriverError> {
982        self.store
983            .load_pending_compaction_projections(&self.runtime_id)
984            .await
985            .map_err(|error| {
986                RuntimeDriverError::Internal(format!(
987                    "failed to load compaction projection outbox: {error}"
988                ))
989            })
990    }
991
992    pub(crate) async fn mark_compaction_projection_finalized(
993        &self,
994        projection: &meerkat_core::CompactionProjectionId,
995    ) -> Result<(), RuntimeDriverError> {
996        self.store
997            .mark_compaction_projection_finalized(&self.runtime_id, projection)
998            .await
999            .map_err(|error| {
1000                RuntimeDriverError::Internal(format!(
1001                    "failed to finalize compaction projection outbox: {error}"
1002                ))
1003            })
1004    }
1005
1006    pub(crate) async fn load_compaction_checkpoint_snapshot(
1007        &self,
1008    ) -> Result<Option<Arc<Vec<u8>>>, RuntimeDriverError> {
1009        self.store
1010            .load_session_snapshot(&self.runtime_id)
1011            .await
1012            .map_err(|error| {
1013                RuntimeDriverError::Internal(format!(
1014                    "failed to load authoritative compaction checkpoint snapshot: {error}"
1015                ))
1016            })
1017    }
1018
1019    pub(crate) async fn commit_compaction_checkpoint_snapshot(
1020        &self,
1021        session_snapshot: Arc<Vec<u8>>,
1022    ) -> Result<(), RuntimeDriverError> {
1023        self.store
1024            .commit_session_snapshot(
1025                &self.runtime_id,
1026                crate::store::SerializedSessionSnapshot { session_snapshot },
1027            )
1028            .await
1029            .map_err(|error| {
1030                RuntimeDriverError::Internal(format!(
1031                    "failed to prepare authoritative compaction checkpoint snapshot: {error}"
1032                ))
1033            })
1034    }
1035
1036    pub fn silent_comms_intents(&self) -> Vec<String> {
1037        self.inner.silent_comms_intents()
1038    }
1039
1040    /// Check if the runtime is idle (delegates to inner).
1041    pub fn is_idle(&self) -> bool {
1042        self.inner.is_idle()
1043    }
1044
1045    /// Ask generated MeerkatMachine authority for the store-visible lifecycle.
1046    fn runtime_state_for_persistence(&self) -> Result<RuntimeState, RuntimeDriverError> {
1047        Self::runtime_state_for_persistence_from_inner(&self.inner)
1048    }
1049
1050    fn runtime_state_for_persistence_from_inner(
1051        inner: &EphemeralRuntimeDriver,
1052    ) -> Result<RuntimeState, RuntimeDriverError> {
1053        crate::meerkat_machine::classify_runtime_lifecycle_durable_state_with_pre_run_phase(
1054            inner.runtime_state(),
1055            inner.pre_run_phase(),
1056        )
1057        .map_err(|err| {
1058            RuntimeDriverError::Internal(format!(
1059                "generated runtime lifecycle durability classification failed: {err}"
1060            ))
1061        })
1062    }
1063
1064    fn lifecycle_commit_for_persistence(
1065        &self,
1066    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
1067        Self::lifecycle_commit_for_persistence_from_inner(&self.inner)
1068    }
1069
1070    fn lifecycle_commit_for_persistence_with_supervisor_authority(
1071        &self,
1072        supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
1073    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
1074        Ok(
1075            MachineLifecycleCommit::new_with_binding_and_unregister_progress(
1076                Self::runtime_state_for_persistence_from_inner(&self.inner)?,
1077                self.inner.machine_lifecycle_binding_facts(),
1078                supervisor_authority,
1079                Self::unregister_progress_for_persistence_from_inner(&self.inner),
1080            ),
1081        )
1082    }
1083
1084    fn lifecycle_commit_for_persistence_from_inner(
1085        inner: &EphemeralRuntimeDriver,
1086    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
1087        Ok(
1088            MachineLifecycleCommit::new_with_binding_and_unregister_progress(
1089                Self::runtime_state_for_persistence_from_inner(inner)?,
1090                inner.machine_lifecycle_binding_facts(),
1091                inner.supervisor_authority_snapshot(),
1092                Self::unregister_progress_for_persistence_from_inner(inner),
1093            ),
1094        )
1095    }
1096
1097    /// Project a committed final `UnregisterSession` for durable storage.
1098    ///
1099    /// The live entry deliberately keeps `registration_phase = Draining` as a
1100    /// same-process rematerialization tombstone until exact entry removal. That
1101    /// mechanical fence is not durable unregister progress: final generated
1102    /// authority has cleared the session binding and all drain obligations, so
1103    /// persisting a progress row would make a later process replay a completed
1104    /// teardown and reject fresh registration.
1105    fn lifecycle_commit_for_completed_unregister(
1106        &self,
1107    ) -> Result<MachineLifecycleCommit, RuntimeDriverError> {
1108        let completed = {
1109            let authority = self.inner.shared_dsl_authority();
1110            let authority = authority
1111                .lock()
1112                .unwrap_or_else(std::sync::PoisonError::into_inner);
1113            let state = authority.state();
1114            state.registration_phase == crate::meerkat_machine::dsl::RegistrationPhase::Draining
1115                && state.session_id.is_none()
1116                && state.active_runtime_id.is_none()
1117                && state.active_fence_token.is_none()
1118                && state.active_runtime_generation.is_none()
1119                && state.active_runtime_epoch_id.is_none()
1120                && !state.unregister_runtime_loop_drain_pending
1121                && !state.unregister_comms_drain_exit_pending
1122                && !state.unregister_completion_waiter_drain_pending
1123        };
1124        if !completed {
1125            return Err(RuntimeDriverError::Internal(
1126                "completed unregister persistence requires the generated final lifecycle image"
1127                    .to_string(),
1128            ));
1129        }
1130        Ok(
1131            MachineLifecycleCommit::new_with_binding_and_unregister_progress(
1132                Self::runtime_state_for_persistence_from_inner(&self.inner)?,
1133                self.inner.machine_lifecycle_binding_facts(),
1134                self.inner.supervisor_authority_snapshot(),
1135                None,
1136            ),
1137        )
1138    }
1139
1140    fn unregister_progress_for_persistence_from_inner(
1141        inner: &EphemeralRuntimeDriver,
1142    ) -> Option<crate::store::MachineUnregisterProgressSnapshot> {
1143        let authority = inner.shared_dsl_authority();
1144        let authority = authority
1145            .lock()
1146            .unwrap_or_else(std::sync::PoisonError::into_inner);
1147        let state = authority.state();
1148        (state.registration_phase == crate::meerkat_machine::dsl::RegistrationPhase::Draining).then(
1149            || {
1150                crate::store::MachineUnregisterProgressSnapshot::new(
1151                    state.unregister_runtime_loop_drain_pending,
1152                    state.unregister_comms_drain_exit_pending,
1153                    state.unregister_completion_waiter_drain_pending,
1154                    state.unregister_runtime_loop_forced_abort,
1155                    state.unregister_comms_drain_forced_abort,
1156                )
1157            },
1158        )
1159    }
1160
1161    /// Snapshot + classify the lifecycle persistence payload, restoring the
1162    /// caller's checkpoint on failure.
1163    ///
1164    /// Contract (Dogma K11): every fallible step between a staged `&mut` DSL
1165    /// transition and the rollback-guarded durable commit restores the
1166    /// caller's checkpoint. A bare `?` here would leave the staged lifecycle
1167    /// live in driver state while reporting failure to the caller. The
1168    /// checkpoint is returned on success so the durable commit arm can keep
1169    /// using it.
1170    fn lifecycle_persistence_payload_with_rollback(
1171        &mut self,
1172        checkpoint: Option<super::ephemeral::EphemeralDriverRollbackSnapshot>,
1173        changed_input_ids: &[InputId],
1174        context: &str,
1175    ) -> Result<
1176        (
1177            Option<super::ephemeral::EphemeralDriverRollbackSnapshot>,
1178            Vec<InputStatePersistenceRecord>,
1179            MachineLifecycleCommit,
1180        ),
1181        RuntimeDriverError,
1182    > {
1183        if let Err(err) = self
1184            .inner
1185            .retire_durably_quiescent_terminal_payloads_in(changed_input_ids)
1186        {
1187            return Err(self.post_transition_failure(
1188                checkpoint,
1189                "terminal_payload_retirement",
1190                format!("{context} terminal payload retirement failed: {err}"),
1191            ));
1192        }
1193        let input_states_result = self
1194            .inner
1195            .authorized_stored_input_states_for_ids(changed_input_ids);
1196        #[cfg(test)]
1197        let input_states_result = if self.force_input_snapshot_failure_for_test {
1198            Err(RuntimeDriverError::Internal(
1199                "forced input-state snapshot failure for checkpoint-restore contract test"
1200                    .to_string(),
1201            ))
1202        } else {
1203            input_states_result
1204        };
1205        let input_states = match input_states_result {
1206            Ok(input_states) => input_states,
1207            Err(err) => {
1208                return Err(self.post_transition_failure(
1209                    checkpoint,
1210                    "input_state_materialization",
1211                    format!("{context} input-state snapshot failed: {err}"),
1212                ));
1213            }
1214        };
1215        let commit = match self.lifecycle_commit_for_persistence() {
1216            Ok(commit) => commit,
1217            Err(err) => {
1218                return Err(self.post_transition_failure(
1219                    checkpoint,
1220                    "lifecycle_commit_classification",
1221                    format!("{context} lifecycle commit classification failed: {err}"),
1222                ));
1223            }
1224        };
1225        Ok((checkpoint, input_states, commit))
1226    }
1227
1228    async fn commit_lifecycle_with_rollback(
1229        &mut self,
1230        checkpoint: Option<super::ephemeral::EphemeralDriverRollbackSnapshot>,
1231        changed_input_ids: &[InputId],
1232        target_state: RuntimeState,
1233        context: &str,
1234    ) -> Result<(), RuntimeDriverError> {
1235        // Contract: every fallible step between the staged DSL transition and
1236        // the durable commit restores the caller's checkpoint on failure. A
1237        // bare `?` here would leave the staged lifecycle (e.g. Destroy) live
1238        // in driver state while reporting failure to the caller.
1239        let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
1240            checkpoint,
1241            changed_input_ids,
1242            context,
1243        )?;
1244        let target_durable_state =
1245            match crate::meerkat_machine::classify_runtime_lifecycle_durable_state_with_pre_run_phase(
1246                target_state,
1247                self.inner.pre_run_phase(),
1248            ) {
1249                Ok(target_durable_state) => target_durable_state,
1250                Err(err) => {
1251                    return Err(self.post_transition_failure(
1252                        checkpoint,
1253                        "lifecycle_target_classification",
1254                        format!(
1255                            "{context} generated target lifecycle durability classification failed: {err}"
1256                        ),
1257                    ));
1258                }
1259            };
1260        if commit.runtime_state() != target_durable_state {
1261            return Err(self.post_transition_failure(
1262                checkpoint,
1263                "lifecycle_target_validation",
1264                format!(
1265                    "{context} durable persist target {target_durable_state:?} from live \
1266                     {target_state:?} disagreed with generated lifecycle commit {:?}",
1267                    commit.runtime_state()
1268                ),
1269            ));
1270        }
1271        if let Err(err) = self
1272            .store
1273            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1274            .await
1275        {
1276            return Err(self.post_transition_failure(
1277                checkpoint,
1278                "lifecycle_commit",
1279                format!("{context} persist failed: {err}"),
1280            ));
1281        }
1282        Ok(())
1283    }
1284
1285    pub(crate) async fn publish_service_turn_terminal(
1286        &mut self,
1287        checkpoint: Option<super::ephemeral::EphemeralDriverRollbackSnapshot>,
1288        target_state: RuntimeState,
1289        session: BoundSessionCommit,
1290        receipt: meerkat_core::lifecycle::RunBoundaryReceipt,
1291        owner_session_id: meerkat_core::types::SessionId,
1292    ) -> Result<PreparedRuntimeSessionCommitResult, RuntimeDriverError> {
1293        self.require_durability_ready()?;
1294        let commit = match self.lifecycle_commit_for_persistence() {
1295            Ok(commit) => commit,
1296            Err(error) => {
1297                return Err(self.post_transition_failure(
1298                    checkpoint,
1299                    "service_turn_terminal_lifecycle_classification",
1300                    format!(
1301                        "service turn terminal receipt lifecycle classification failed: {error}"
1302                    ),
1303                ));
1304            }
1305        };
1306        let target_durable_state =
1307            match crate::meerkat_machine::classify_runtime_lifecycle_durable_state(target_state) {
1308                Ok(target_durable_state) => target_durable_state,
1309                Err(error) => {
1310                    return Err(self.post_transition_failure(
1311                        checkpoint,
1312                        "service_turn_terminal_target_classification",
1313                        format!(
1314                            "service turn terminal receipt target classification failed: {error}"
1315                        ),
1316                    ));
1317                }
1318            };
1319        if commit.runtime_state() != target_durable_state {
1320            return Err(self.post_transition_failure(
1321                checkpoint,
1322                "service_turn_terminal_target_validation",
1323                format!(
1324                    "service turn terminal receipt durable target {target_durable_state:?} disagreed with generated lifecycle {:?}",
1325                    commit.runtime_state()
1326                ),
1327            ));
1328        }
1329        let promotion = session.provisional_promotion_receipt().cloned();
1330        let request = match promotion {
1331            Some(checkpoint_receipt) => {
1332                match self.prepare_provisional_promotion(
1333                    &checkpoint_receipt,
1334                    &receipt,
1335                    &owner_session_id,
1336                ) {
1337                    Ok(PreparedProvisionalPromotion::WholeBlob(promotion)) => {
1338                        PreparedRuntimeSessionCommit::promote_whole_blob_service_turn_terminal(
1339                            promotion,
1340                            receipt,
1341                            commit,
1342                            owner_session_id,
1343                        )
1344                    }
1345                    Ok(PreparedProvisionalPromotion::HeadCanonical(promotion)) => {
1346                        PreparedRuntimeSessionCommit::promote_head_canonical_service_turn_terminal(
1347                            promotion,
1348                            receipt,
1349                            commit,
1350                            owner_session_id,
1351                        )
1352                    }
1353                    Err(error) => Err(error),
1354                }
1355            }
1356            None => Ok(PreparedRuntimeSessionCommit::service_turn_terminal(
1357                session,
1358                receipt,
1359                commit,
1360                owner_session_id,
1361            )),
1362        };
1363        let request = match request {
1364            Ok(request) => request,
1365            Err(error) => {
1366                return Err(self.post_transition_failure(
1367                    checkpoint,
1368                    "service_turn_terminal_promotion_validation",
1369                    format!("service turn terminal promotion is invalid: {error}"),
1370                ));
1371            }
1372        };
1373        let result = match self
1374            .store
1375            .commit_prepared_session_boundary(&self.runtime_id, request)
1376            .await
1377        {
1378            Ok(result) => result,
1379            Err(error) => {
1380                return Err(self.post_transition_failure(
1381                    checkpoint,
1382                    "service_turn_terminal_commit",
1383                    format!("service turn terminal receipt persist failed: {error}"),
1384                ));
1385            }
1386        };
1387        self.inner.sync_control_projection_from_dsl_authority();
1388        Ok(result)
1389    }
1390
1391    pub(crate) fn set_control_projection(
1392        &mut self,
1393        next_phase: RuntimeState,
1394        current_run_id: Option<RunId>,
1395        pre_run_phase: Option<RuntimeState>,
1396    ) {
1397        self.inner
1398            .set_control_projection(next_phase, current_run_id, pre_run_phase);
1399    }
1400
1401    /// Low-level control projection shim for external contract tests.
1402    ///
1403    /// This does not decide lifecycle legality; it only applies an already
1404    /// chosen MeerkatMachine control projection to the concrete driver shell.
1405    pub(crate) fn sync_control_projection_from_dsl_authority(&mut self) {
1406        self.inner.sync_control_projection_from_dsl_authority();
1407    }
1408
1409    pub(crate) async fn persist_current_machine_lifecycle(
1410        &mut self,
1411        context: &str,
1412    ) -> Result<(), RuntimeDriverError> {
1413        self.require_durability_ready()?;
1414        let commit = match self.lifecycle_commit_for_persistence() {
1415            Ok(commit) => commit,
1416            Err(error) => {
1417                return Err(self.post_transition_failure(
1418                    None,
1419                    "ordinary_lifecycle_classification",
1420                    format!("{context} lifecycle classification failed: {error}"),
1421                ));
1422            }
1423        };
1424        if let Err(error) = self
1425            .store
1426            .commit_machine_lifecycle(&self.runtime_id, commit, &[])
1427            .await
1428        {
1429            return Err(self.post_transition_failure(
1430                None,
1431                "ordinary_lifecycle_commit",
1432                format!("{context} lifecycle persist failed: {error}"),
1433            ));
1434        }
1435        Ok(())
1436    }
1437
1438    /// Explicit teardown/recovery write that is allowed to operate while an
1439    /// entry is not durability-ready. Callers must already hold the unregister
1440    /// recovery authority and must not roll a possibly-committed ordinary
1441    /// transition back through this seam.
1442    pub(crate) async fn persist_recovery_machine_lifecycle(
1443        &mut self,
1444        context: &str,
1445    ) -> Result<(), RuntimeDriverError> {
1446        let commit = self.lifecycle_commit_for_persistence()?;
1447        self.store
1448            .commit_machine_lifecycle(&self.runtime_id, commit, &[])
1449            .await
1450            .map_err(|error| {
1451                RuntimeDriverError::Internal(format!(
1452                    "{context} recovery lifecycle persist failed: {error}"
1453                ))
1454            })
1455    }
1456
1457    pub(crate) async fn commit_unregister_finalization(
1458        &mut self,
1459        context: &str,
1460        retired_ops_epoch: &meerkat_core::RuntimeEpochId,
1461        authority: crate::meerkat_machine::DeleteOpsFinalizationAuthority,
1462    ) -> Result<(), RuntimeDriverError> {
1463        let commit = self.lifecycle_commit_for_completed_unregister()?;
1464        let finalization = crate::store::UnregisterFinalizationCommit::new(
1465            commit,
1466            Vec::new(),
1467            retired_ops_epoch.clone(),
1468            authority,
1469        );
1470        self.store
1471            .commit_unregister_finalization(&self.runtime_id, finalization)
1472            .await
1473            .map_err(|err| match err {
1474                crate::store::RuntimeStoreError::UnregisterFinalizationOutcomeUnknown(reason) => {
1475                    RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
1476                        reason: format!("{context} lifecycle+ops finalization: {reason}"),
1477                    }
1478                }
1479                err => RuntimeDriverError::Internal(format!(
1480                    "{context} lifecycle+ops finalization failed: {err}"
1481                )),
1482            })
1483    }
1484
1485    pub(crate) async fn persist_completed_unregister_machine_lifecycle(
1486        &mut self,
1487        context: &str,
1488        _authority: crate::meerkat_machine::RetainOpsFinalizationAuthority,
1489    ) -> Result<(), RuntimeDriverError> {
1490        let commit = self.lifecycle_commit_for_completed_unregister()?;
1491        self.store
1492            .commit_machine_lifecycle(&self.runtime_id, commit, &[])
1493            .await
1494            .map_err(|error| {
1495                // The generic lifecycle commit contract is atomic, but unlike
1496                // commit_unregister_finalization it does not distinguish a
1497                // definitely-uncommitted error from a lost acknowledgement.
1498                // RetainSnapshot finalization must therefore treat every
1499                // error as ambiguous: rolling local authority back to
1500                // Draining could overwrite a terminal image that already
1501                // committed durably.
1502                RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
1503                    reason: format!(
1504                        "{context} retained lifecycle finalization acknowledgement unavailable: {error}"
1505                    ),
1506                }
1507            })
1508    }
1509
1510    /// Persist a previewed closed supervisor projection alongside the current
1511    /// machine lifecycle. This lets the supervisor saga commit durable truth
1512    /// before changing the shared live authority, avoiding a whole-authority
1513    /// rollback across asynchronous store I/O (peer ingress may concurrently
1514    /// mutate unrelated generated fields).
1515    pub(crate) async fn persist_current_machine_lifecycle_with_supervisor_authority(
1516        &mut self,
1517        context: &str,
1518        supervisor_authority: crate::store::SupervisorAuthoritySnapshot,
1519    ) -> Result<(), RuntimeDriverError> {
1520        self.require_durability_ready()?;
1521        let commit = match self
1522            .lifecycle_commit_for_persistence_with_supervisor_authority(supervisor_authority)
1523        {
1524            Ok(commit) => commit,
1525            Err(error) => {
1526                return Err(self.post_transition_failure(
1527                    None,
1528                    "supervisor_lifecycle_classification",
1529                    format!("{context} lifecycle classification failed: {error}"),
1530                ));
1531            }
1532        };
1533        if let Err(error) = self
1534            .store
1535            .commit_machine_lifecycle(&self.runtime_id, commit, &[])
1536            .await
1537        {
1538            return Err(self.post_transition_failure(
1539                None,
1540                "supervisor_lifecycle_commit",
1541                format!("{context} lifecycle persist failed: {error}"),
1542            ));
1543        }
1544        Ok(())
1545    }
1546
1547    /// Contract helper for external tests that need to start a run through the
1548    /// same DSL authority used by the runtime loop.
1549    #[doc(hidden)]
1550    pub fn contract_begin_run_authority(
1551        &mut self,
1552        run_id: RunId,
1553    ) -> Result<(), RuntimeDriverError> {
1554        self.inner.contract_begin_run_authority(run_id)
1555    }
1556
1557    /// Get pending events (delegates to inner).
1558    pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
1559        self.inner.drain_events()
1560    }
1561
1562    /// Drain the typed post-admission signal (delegates to inner).
1563    pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
1564        self.inner.take_post_admission_signal()
1565    }
1566
1567    /// Inspect the current typed post-admission signal without draining it.
1568    pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
1569        self.inner.post_admission_signal()
1570    }
1571
1572    /// Check and clear wake flag (backward-compat, delegates to inner).
1573    pub fn take_wake_requested(&mut self) -> bool {
1574        self.inner.take_wake_requested()
1575    }
1576
1577    /// Check and clear immediate processing flag (backward-compat, delegates to inner).
1578    pub fn take_process_requested(&mut self) -> bool {
1579        self.inner.take_process_requested()
1580    }
1581
1582    /// Contract helper for recovery/queue-projection tests. Production runtime
1583    /// execution must use generated batch authority via `dequeue_batch_exact`.
1584    #[cfg(any(test, debug_assertions, feature = "test-support"))]
1585    #[doc(hidden)]
1586    pub fn contract_dequeue_next_for_recovery_tests(&mut self) -> Option<(InputId, Input)> {
1587        self.inner.contract_dequeue_next_for_recovery_tests()
1588    }
1589
1590    pub(crate) fn dequeue_batch_exact(
1591        &mut self,
1592        batch: &crate::meerkat_machine::driver::AuthorizedRuntimeLoopBatch,
1593    ) -> Result<Vec<(InputId, Input)>, RuntimeDriverError> {
1594        self.inner.dequeue_batch_exact(batch)
1595    }
1596
1597    pub fn has_queued_input_outside(&self, excluded: &[InputId]) -> bool {
1598        self.inner.has_queued_input_outside(excluded)
1599    }
1600
1601    pub(crate) fn defer_queued_inputs_behind_backlog(
1602        &mut self,
1603        input_ids: &[InputId],
1604    ) -> Result<(), RuntimeDriverError> {
1605        self.inner.defer_queued_inputs_behind_backlog(input_ids)
1606    }
1607
1608    pub(crate) fn absorb_post_admission_effects(
1609        &mut self,
1610        effects: &[crate::meerkat_machine::dsl::MeerkatMachineEffect],
1611    ) {
1612        self.inner.absorb_post_admission_effects(effects);
1613    }
1614
1615    pub(crate) fn resolve_admission(
1616        &self,
1617        input: &Input,
1618    ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
1619        self.inner.resolve_admission(input)
1620    }
1621
1622    pub(crate) fn resolve_admission_with_active_turn_boundary(
1623        &self,
1624        input: &Input,
1625        active_turn_boundary_available: bool,
1626    ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
1627        self.inner
1628            .resolve_admission_with_active_turn_boundary(input, active_turn_boundary_available)
1629    }
1630
1631    pub(crate) fn resolve_admission_without_wake_with_active_turn_boundary(
1632        &self,
1633        input: &Input,
1634        active_turn_boundary_available: bool,
1635    ) -> Result<crate::accept::ResolvedAdmission, RuntimeDriverError> {
1636        self.inner
1637            .resolve_admission_without_wake_with_active_turn_boundary(
1638                input,
1639                active_turn_boundary_available,
1640            )
1641    }
1642
1643    pub(crate) async fn accept_resolved_input(
1644        &mut self,
1645        input: Input,
1646        resolved: crate::accept::ResolvedAdmission,
1647    ) -> Result<AcceptOutcome, RuntimeDriverError> {
1648        self.require_durability_ready()?;
1649        self.inner.ensure_contract_session_authority()?;
1650        if let Some((existing_id, existing_seed)) =
1651            self.durable_idempotency_duplicate(&input).await?
1652        {
1653            let input_id = input.id().clone();
1654            self.inner
1655                .record_durable_idempotency_deduplication(input_id.clone(), existing_id.clone());
1656            return Ok(AcceptOutcome::Deduplicated {
1657                input_id,
1658                existing_id,
1659                existing_seed,
1660            });
1661        }
1662        let preview = self
1663            .inner
1664            .preview_accept_resolved_input_bounded(&input, &resolved)?;
1665        let AcceptOutcome::Accepted {
1666            input_id: expected_input_id,
1667            ..
1668        } = preview
1669        else {
1670            return self.inner.accept_resolved_input(input, resolved).await;
1671        };
1672
1673        let flags = resolved.coarse_flags();
1674        let changed_input_ids = resolved.persistence_changed_input_ids(&expected_input_id);
1675        let mut input_for_recovery = input.clone();
1676        externalize_input_images(self.blob_store.as_ref(), &mut input_for_recovery)
1677            .await
1678            .map_err(|err| {
1679                RuntimeDriverError::Internal(format!(
1680                    "failed to externalize runtime input images: {err}"
1681                ))
1682            })?;
1683
1684        // Production registrations carry no rollback image: mutate once, then
1685        // either commit the exact one/two-row admission delta or degrade the
1686        // shared entry to ReloadRequired. Direct/test constructors retain one
1687        // compatibility checkpoint.
1688        let checkpoint = self.persistence_rollback_checkpoint();
1689        let mut outcome = match self.inner.accept_resolved_input(input, resolved).await {
1690            Ok(outcome) => outcome,
1691            Err(error) => {
1692                return Err(self.post_transition_failure(
1693                    checkpoint,
1694                    "admission_apply",
1695                    error.to_string(),
1696                ));
1697            }
1698        };
1699        let AcceptOutcome::Accepted {
1700            ref input_id,
1701            ref mut state,
1702            ref mut seed,
1703            ..
1704        } = outcome
1705        else {
1706            return Err(self.post_transition_failure(
1707                checkpoint,
1708                "admission_outcome_validation",
1709                format!(
1710                    "accepted admission preview for {expected_input_id} committed as {outcome:?}"
1711                ),
1712            ));
1713        };
1714        if input_id != &expected_input_id {
1715            return Err(self.post_transition_failure(
1716                checkpoint,
1717                "admission_identity_validation",
1718                format!(
1719                    "accepted admission preview named {expected_input_id} but committed {input_id}"
1720                ),
1721            ));
1722        }
1723        if let Err(error) = self
1724            .inner
1725            .machine_apply_accept_with_completion_signal(input_id, flags)
1726        {
1727            return Err(self.post_transition_failure(
1728                checkpoint,
1729                "admission_completion_signal",
1730                error.to_string(),
1731            ));
1732        }
1733        let Some(mut bundle) = self.inner.stored_input_state(input_id) else {
1734            return Err(self.post_transition_failure(
1735                checkpoint,
1736                "admission_input_materialization",
1737                format!("generated input lifecycle phase missing for accepted input {input_id}"),
1738            ));
1739        };
1740        bundle.state.persisted_input = Some(input_for_recovery);
1741        self.inner.ledger_mut().accept(bundle.state.clone());
1742        *state = bundle.state;
1743        *seed = bundle.seed;
1744
1745        // Admission may atomically supersede/coalesce an older queued row.
1746        // Retire that terminal row's payload in this same admission delta;
1747        // doing it after the write would strand one full historical prompt
1748        // per replacement even though the live row is immediately archived.
1749        if let Err(error) = self
1750            .inner
1751            .retire_durably_quiescent_terminal_payloads_in(&changed_input_ids)
1752        {
1753            return Err(self.post_transition_failure(
1754                checkpoint,
1755                "admission_terminal_payload_retirement",
1756                error.to_string(),
1757            ));
1758        }
1759        let records = match self
1760            .inner
1761            .authorized_stored_input_states_for_ids(&changed_input_ids)
1762        {
1763            Ok(records) => records,
1764            Err(error) => {
1765                return Err(self.post_transition_failure(
1766                    checkpoint,
1767                    "admission_delta_materialization",
1768                    error.to_string(),
1769                ));
1770            }
1771        };
1772        if let Err(error) = self
1773            .store
1774            .persist_input_states_atomically(&self.runtime_id, &records)
1775            .await
1776        {
1777            return Err(self.post_transition_failure(
1778                checkpoint,
1779                "admission_commit",
1780                format!("atomic admission delta persist failed: {error}"),
1781            ));
1782        }
1783        let terminal_input_ids = match self
1784            .inner
1785            .archivable_terminal_input_ids_in(&changed_input_ids)
1786        {
1787            Ok(input_ids) => input_ids,
1788            Err(error) => {
1789                return Err(self.post_transition_failure(
1790                    None,
1791                    "admission_terminal_classification",
1792                    error.to_string(),
1793                ));
1794            }
1795        };
1796        if let Err(error) = self
1797            .inner
1798            .archive_archivable_terminal_inputs_after_durable_commit(&terminal_input_ids)
1799        {
1800            return Err(self.post_transition_failure(
1801                None,
1802                "admission_terminal_archive",
1803                error.to_string(),
1804            ));
1805        }
1806
1807        Ok(outcome)
1808    }
1809
1810    pub(crate) async fn preview_accept_resolved_input(
1811        &self,
1812        input: Input,
1813        resolved: &crate::accept::ResolvedAdmission,
1814    ) -> Result<AcceptOutcome, RuntimeDriverError> {
1815        self.require_durability_ready()?;
1816        if let Some((existing_id, existing_seed)) =
1817            self.durable_idempotency_duplicate(&input).await?
1818        {
1819            return Ok(AcceptOutcome::Deduplicated {
1820                input_id: input.id().clone(),
1821                existing_id,
1822                existing_seed,
1823            });
1824        }
1825        self.inner
1826            .preview_accept_resolved_input_bounded(&input, resolved)
1827    }
1828
1829    pub(crate) fn machine_realize_authorized_stage_batch(
1830        &mut self,
1831        authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
1832    ) -> Result<(), crate::traits::RuntimeDriverError> {
1833        self.inner.machine_realize_authorized_stage_batch(authority)
1834    }
1835
1836    pub(crate) async fn machine_normalize_live_boundary_unavailable(
1837        &mut self,
1838        input_id: &InputId,
1839    ) -> Result<(), RuntimeDriverError> {
1840        self.require_durability_ready()?;
1841        let checkpoint = self.persistence_rollback_checkpoint();
1842        if let Err(error) = self
1843            .inner
1844            .machine_normalize_live_boundary_unavailable(input_id)
1845        {
1846            return Err(self.post_transition_failure(
1847                checkpoint,
1848                "live_boundary_unavailable_normalization",
1849                error.to_string(),
1850            ));
1851        }
1852        let records = match self
1853            .inner
1854            .authorized_stored_input_states_for_ids(std::slice::from_ref(input_id))
1855        {
1856            Ok(records) => records,
1857            Err(error) => {
1858                return Err(self.post_transition_failure(
1859                    checkpoint,
1860                    "live_boundary_unavailable_materialization",
1861                    error.to_string(),
1862                ));
1863            }
1864        };
1865        if let Err(error) = self
1866            .store
1867            .persist_input_states_atomically(&self.runtime_id, &records)
1868            .await
1869        {
1870            return Err(self.post_transition_failure(
1871                checkpoint,
1872                "live_boundary_unavailable_commit",
1873                format!("unavailable-boundary input normalization persist failed: {error}"),
1874            ));
1875        }
1876        Ok(())
1877    }
1878
1879    /// Apply input (delegates to inner).
1880    pub fn apply_input(
1881        &mut self,
1882        input_id: &InputId,
1883        run_id: &meerkat_core::lifecycle::RunId,
1884    ) -> Result<(), crate::traits::RuntimeDriverError> {
1885        self.inner.apply_input(input_id, run_id)
1886    }
1887
1888    pub(crate) fn machine_realize_terminal_failure_applied(
1889        &mut self,
1890        run_id: &meerkat_core::lifecycle::RunId,
1891        input_ids: &[InputId],
1892    ) -> Result<(), crate::traits::RuntimeDriverError> {
1893        self.inner
1894            .machine_realize_terminal_failure_applied(run_id, input_ids)
1895    }
1896
1897    /// Roll back staged inputs (delegates to inner).
1898    pub fn rollback_staged(
1899        &mut self,
1900        input_ids: &[InputId],
1901    ) -> Result<(), crate::traits::RuntimeDriverError> {
1902        self.inner.rollback_staged(input_ids)
1903    }
1904
1905    /// Persist the just-staged run bindings BEFORE the run executes.
1906    ///
1907    /// `StageForRun` binds each contributing input to the run inside the
1908    /// generated machine, but that fact was previously durable only with the
1909    /// boundary commit — so a crash mid-run left the executed turn's inputs
1910    /// durably unbound, indistinguishable by identity from freshly queued
1911    /// work. Recovery refuses to guess (text is content evidence, never
1912    /// identity) and would hold such a tail; making the binding durable at
1913    /// staging closes that window for every run started by this binary.
1914    /// Fail-closed: a persist failure aborts the run start.
1915    pub(crate) async fn persist_staged_input_bindings(
1916        &self,
1917        input_ids: &[InputId],
1918    ) -> Result<(), RuntimeDriverError> {
1919        self.require_durability_ready()?;
1920        let records = self
1921            .inner
1922            .authorized_stored_input_states_for_ids(input_ids)?;
1923        if records.is_empty() {
1924            return Ok(());
1925        }
1926        match self
1927            .store
1928            .persist_input_states_atomically(&self.runtime_id, &records)
1929            .await
1930        {
1931            Ok(()) => Ok(()),
1932            Err(error) => Err(self.mark_durability_reload_required(
1933                "staged_input_binding_commit",
1934                format!("atomic staged input binding persist failed: {error}"),
1935            )),
1936        }
1937    }
1938
1939    pub(crate) async fn abandon_pending_inputs(
1940        &mut self,
1941        reason: InputAbandonReason,
1942    ) -> Result<usize, RuntimeDriverError> {
1943        self.require_durability_ready()?;
1944        let changed_input_ids = self.inner.active_input_ids();
1945        let checkpoint = self.persistence_rollback_checkpoint();
1946        let abandoned = match self.inner.abandon_pending_inputs(reason) {
1947            Ok(abandoned) => abandoned,
1948            Err(err) => {
1949                return Err(self.post_transition_failure(
1950                    checkpoint,
1951                    "abandon_pending_inputs",
1952                    err.to_string(),
1953                ));
1954            }
1955        };
1956        let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
1957            checkpoint,
1958            &changed_input_ids,
1959            "pending input abandon",
1960        )?;
1961        if let Err(err) = self
1962            .store
1963            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
1964            .await
1965        {
1966            return Err(self.post_transition_failure(
1967                checkpoint,
1968                "abandon_pending_inputs_commit",
1969                format!("pending input abandon persist failed: {err}"),
1970            ));
1971        }
1972        if let Err(error) = self
1973            .inner
1974            .archive_archivable_terminal_inputs_after_durable_commit(&changed_input_ids)
1975        {
1976            return Err(self.post_transition_failure(
1977                None,
1978                "abandon_pending_inputs_archive",
1979                error.to_string(),
1980            ));
1981        }
1982        Ok(abandoned)
1983    }
1984
1985    pub(crate) async fn abandon_queued_input(
1986        &mut self,
1987        input_id: &meerkat_core::lifecycle::InputId,
1988        reason: InputAbandonReason,
1989    ) -> Result<bool, RuntimeDriverError> {
1990        self.require_durability_ready()?;
1991        let checkpoint = self.persistence_rollback_checkpoint();
1992        let abandoned = match self.inner.abandon_queued_input(input_id, reason) {
1993            Ok(abandoned) => abandoned,
1994            Err(error) => {
1995                return Err(self.post_transition_failure(
1996                    checkpoint,
1997                    "abandon_queued_input",
1998                    error.to_string(),
1999                ));
2000            }
2001        };
2002        if !abandoned {
2003            return Ok(false);
2004        }
2005        let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
2006            checkpoint,
2007            std::slice::from_ref(input_id),
2008            "tracked input cancel",
2009        )?;
2010        if let Err(error) = self
2011            .store
2012            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
2013            .await
2014        {
2015            return Err(self.post_transition_failure(
2016                checkpoint,
2017                "abandon_queued_input_commit",
2018                format!("tracked input cancel persist failed: {error}"),
2019            ));
2020        }
2021        if let Err(error) = self
2022            .inner
2023            .archive_archivable_terminal_inputs_after_durable_commit(std::slice::from_ref(input_id))
2024        {
2025            return Err(self.post_transition_failure(
2026                None,
2027                "abandon_queued_input_archive",
2028                error.to_string(),
2029            ));
2030        }
2031        Ok(true)
2032    }
2033
2034    /// Recycle the in-memory driver shell while preserving canonical pending
2035    /// work from durable runtime truth.
2036    ///
2037    /// Unlike `reset()`, this must not abandon queued/staged work.
2038    pub(crate) async fn recycle_preserving_work(&mut self) -> Result<usize, RuntimeDriverError> {
2039        self.require_durability_ready()?;
2040        let checkpoint = self.persistence_rollback_checkpoint();
2041        let transferred = match self.inner.recycle_preserving_work() {
2042            Ok(transferred) => transferred,
2043            Err(err) => {
2044                return Err(self.post_transition_failure(
2045                    checkpoint,
2046                    "recycle_preserving_work",
2047                    err.to_string(),
2048                ));
2049            }
2050        };
2051        let (checkpoint, input_states, commit) =
2052            self.lifecycle_persistence_payload_with_rollback(checkpoint, &[], "recycle")?;
2053        if let Err(err) = self
2054            .store
2055            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
2056            .await
2057        {
2058            return Err(self.post_transition_failure(
2059                checkpoint,
2060                "recycle_commit",
2061                format!("recycle persist failed: {err}"),
2062            ));
2063        }
2064
2065        self.inner.sync_control_projection_from_dsl_authority();
2066        Ok(transferred)
2067    }
2068
2069    pub(crate) async fn realize_retire_lifecycle(
2070        &mut self,
2071    ) -> Result<crate::traits::RetireReport, RuntimeDriverError> {
2072        self.require_durability_ready()?;
2073        let checkpoint = self.persistence_rollback_checkpoint();
2074        let report = self.inner.finalize_retire();
2075        // Restore the checkpoint on classification failure: an early `?` here
2076        // would leave the finalized retire state live without rollback.
2077        let target_state = match self.runtime_state_for_persistence() {
2078            Ok(target_state) => target_state,
2079            Err(err) => {
2080                return Err(self.post_transition_failure(
2081                    checkpoint,
2082                    "retire_lifecycle_classification",
2083                    err.to_string(),
2084                ));
2085            }
2086        };
2087        self.commit_lifecycle_with_rollback(checkpoint, &[], target_state, "retire")
2088            .await?;
2089        self.inner.sync_control_projection_from_dsl_authority();
2090        Ok(report)
2091    }
2092
2093    pub(crate) async fn realize_reset_lifecycle(
2094        &mut self,
2095    ) -> Result<crate::traits::ResetReport, RuntimeDriverError> {
2096        self.require_durability_ready()?;
2097        let changed_input_ids = self.inner.active_input_ids();
2098        let checkpoint = self.persistence_rollback_checkpoint();
2099        let report = match self.inner.reset_cleanup() {
2100            Ok(report) => report,
2101            Err(err) => {
2102                return Err(self.post_transition_failure(
2103                    checkpoint,
2104                    "reset_cleanup",
2105                    err.to_string(),
2106                ));
2107            }
2108        };
2109        // Restore the checkpoint on classification failure: an early `?` here
2110        // would leave the reset-cleaned state live without rollback.
2111        let target_state = match self.runtime_state_for_persistence() {
2112            Ok(target_state) => target_state,
2113            Err(err) => {
2114                return Err(self.post_transition_failure(
2115                    checkpoint,
2116                    "reset_lifecycle_classification",
2117                    err.to_string(),
2118                ));
2119            }
2120        };
2121        self.commit_lifecycle_with_rollback(checkpoint, &changed_input_ids, target_state, "reset")
2122            .await?;
2123        if let Err(error) = self
2124            .inner
2125            .archive_archivable_terminal_inputs_after_durable_commit(&changed_input_ids)
2126        {
2127            return Err(self.post_transition_failure(
2128                None,
2129                "reset_terminal_archive",
2130                error.to_string(),
2131            ));
2132        }
2133        self.inner.sync_control_projection_from_dsl_authority();
2134        Ok(report)
2135    }
2136
2137    pub(crate) fn prepare_destroy_lifecycle(
2138        &mut self,
2139    ) -> Result<(Vec<InputId>, DestroyReport), RuntimeDriverError> {
2140        self.require_durability_ready()?;
2141        let changed_input_ids = self.inner.active_input_ids();
2142        let abandoned = match self.inner.destroy_cleanup() {
2143            Ok(abandoned) => abandoned,
2144            Err(err) => {
2145                return Err(self.post_transition_failure(None, "destroy_cleanup", err.to_string()));
2146            }
2147        };
2148        Ok((
2149            changed_input_ids,
2150            DestroyReport {
2151                inputs_abandoned: abandoned,
2152            },
2153        ))
2154    }
2155
2156    pub(crate) async fn commit_prepared_destroy_lifecycle(
2157        &mut self,
2158        changed_input_ids: Vec<InputId>,
2159    ) -> Result<(), RuntimeDriverError> {
2160        self.require_durability_ready()?;
2161        let target_state = match self.runtime_state_for_persistence() {
2162            Ok(target_state) => target_state,
2163            Err(err) => {
2164                return Err(self.post_transition_failure(
2165                    None,
2166                    "destroy_lifecycle_classification",
2167                    err.to_string(),
2168                ));
2169            }
2170        };
2171        self.commit_lifecycle_with_rollback(None, &changed_input_ids, target_state, "destroy")
2172            .await?;
2173        if let Err(error) = self
2174            .inner
2175            .archive_archivable_terminal_inputs_after_durable_commit(&changed_input_ids)
2176        {
2177            return Err(self.post_transition_failure(
2178                None,
2179                "destroy_terminal_archive",
2180                error.to_string(),
2181            ));
2182        }
2183        self.inner.sync_control_projection_from_dsl_authority();
2184        Ok(())
2185    }
2186
2187    pub(crate) fn rollback_prepared_destroy_lifecycle(&self) -> RuntimeDriverError {
2188        self.mark_durability_reload_required(
2189            "destroy_preparation_rollback",
2190            "prepared destroy could not reach its durable commit boundary",
2191        )
2192    }
2193
2194    pub(crate) async fn finalize_runtime_executor_exit(
2195        &mut self,
2196    ) -> Result<(), RuntimeDriverError> {
2197        self.require_durability_ready()?;
2198        let changed_input_ids = self.inner.active_input_ids();
2199        let checkpoint = self.persistence_rollback_checkpoint();
2200        if let Err(err) = self.inner.apply_runtime_executor_exited_authority() {
2201            return Err(self.post_transition_failure(
2202                checkpoint,
2203                "runtime_executor_exit",
2204                err.to_string(),
2205            ));
2206        }
2207        if let Err(err) = self.inner.stop_runtime_cleanup() {
2208            return Err(self.post_transition_failure(
2209                checkpoint,
2210                "stop_runtime_cleanup",
2211                err.to_string(),
2212            ));
2213        }
2214        // Resolve the durable target BEFORE handing the checkpoint to the
2215        // commit helper, so a classification failure restores the staged
2216        // executor-exit state instead of leaving it live without rollback.
2217        let target_state = match self.runtime_state_for_persistence() {
2218            Ok(target_state) => target_state,
2219            Err(err) => {
2220                return Err(self.post_transition_failure(
2221                    checkpoint,
2222                    "stop_lifecycle_classification",
2223                    err.to_string(),
2224                ));
2225            }
2226        };
2227        self.commit_lifecycle_with_rollback(checkpoint, &changed_input_ids, target_state, "stop")
2228            .await?;
2229        if let Err(error) = self
2230            .inner
2231            .archive_archivable_terminal_inputs_after_durable_commit(&changed_input_ids)
2232        {
2233            return Err(self.post_transition_failure(
2234                None,
2235                "stop_terminal_archive",
2236                error.to_string(),
2237            ));
2238        }
2239        self.inner.sync_control_projection_from_dsl_authority();
2240        Ok(())
2241    }
2242
2243    pub(crate) fn machine_realize_boundary_applied_in_memory(
2244        &mut self,
2245        run_id: &RunId,
2246        receipt: &RunBoundaryReceipt,
2247    ) -> Result<(), RuntimeDriverError> {
2248        self.inner.machine_realize_boundary_applied(run_id, receipt)
2249    }
2250
2251    pub(crate) fn machine_realize_run_completed_in_memory(
2252        &mut self,
2253        run_id: &RunId,
2254        consumed_input_ids: &[InputId],
2255    ) -> Result<(), RuntimeDriverError> {
2256        self.inner
2257            .machine_realize_run_completed(run_id, consumed_input_ids)
2258    }
2259
2260    pub(crate) async fn machine_realize_live_boundary_context_injected(
2261        &mut self,
2262        run_id: &RunId,
2263        input_ids: &[InputId],
2264        stage_authority: crate::meerkat_machine::driver::AuthorizedStageForRun,
2265        session: Option<BoundSessionCommit>,
2266        owner_session_id: &meerkat_core::types::SessionId,
2267    ) -> Result<PreparedRuntimeSessionCommitResult, RuntimeDriverError> {
2268        self.require_durability_ready()?;
2269        let checkpoint = self.persistence_rollback_checkpoint();
2270        let receipt = match self.inner.machine_realize_live_boundary_context_injected(
2271            run_id,
2272            input_ids,
2273            stage_authority,
2274        ) {
2275            Ok(receipt) => receipt,
2276            Err(err) => {
2277                return Err(self.post_transition_failure(
2278                    checkpoint,
2279                    "live_boundary_realization",
2280                    err.to_string(),
2281                ));
2282            }
2283        };
2284        let input_updates = match self.inner.authorized_stored_input_states_for_ids(input_ids) {
2285            Ok(input_updates) => input_updates,
2286            Err(err) => {
2287                return Err(self.post_transition_failure(
2288                    checkpoint,
2289                    "live_boundary_input_materialization",
2290                    err.to_string(),
2291                ));
2292            }
2293        };
2294        let request = match self.prepare_success_boundary(
2295            session,
2296            receipt.clone(),
2297            input_updates,
2298            owner_session_id.clone(),
2299        ) {
2300            Ok(request) => request,
2301            Err(error) => {
2302                return Err(self.post_transition_failure(
2303                    checkpoint,
2304                    "live_boundary_promotion_validation",
2305                    format!("runtime live-boundary promotion is invalid: {error}"),
2306                ));
2307            }
2308        };
2309        let result = match self
2310            .store
2311            .commit_prepared_session_boundary(&self.runtime_id, request)
2312            .await
2313        {
2314            Ok(result) => result,
2315            Err(err) => {
2316                return Err(self.post_transition_failure(
2317                    checkpoint,
2318                    "live_boundary_commit",
2319                    format!("runtime live-boundary context commit failed: {err}"),
2320                ));
2321            }
2322        };
2323        Ok(result)
2324    }
2325
2326    pub(crate) async fn machine_commit_completed_boundary_snapshot(
2327        &mut self,
2328        receipt: &RunBoundaryReceipt,
2329        session: Option<BoundSessionCommit>,
2330        owner_session_id: &meerkat_core::types::SessionId,
2331    ) -> Result<PreparedRuntimeSessionCommitResult, RuntimeDriverError> {
2332        self.require_durability_ready()?;
2333        let input_updates = self
2334            .inner
2335            .authorized_stored_input_states_for_ids(&receipt.contributing_input_ids)?;
2336        let request = self
2337            .prepare_success_boundary(
2338                session,
2339                receipt.clone(),
2340                input_updates,
2341                owner_session_id.clone(),
2342            )
2343            .map_err(|error| {
2344                self.post_transition_failure(
2345                    None,
2346                    "completed_boundary_promotion_validation",
2347                    format!("runtime completed-boundary promotion is invalid: {error}"),
2348                )
2349            })?;
2350        let result = self
2351            .store
2352            .commit_prepared_session_boundary(&self.runtime_id, request)
2353            .await
2354            .map_err(|e| {
2355                self.post_transition_failure(
2356                    None,
2357                    "completed_boundary_commit",
2358                    format!("runtime completed-boundary commit failed: {e}"),
2359                )
2360            })?;
2361        if let Err(error) = self
2362            .inner
2363            .archive_archivable_terminal_inputs_after_durable_commit(
2364                &receipt.contributing_input_ids,
2365            )
2366        {
2367            return Err(self.post_transition_failure(
2368                None,
2369                "completed_boundary_archive",
2370                error.to_string(),
2371            ));
2372        }
2373        Ok(result)
2374    }
2375
2376    /// Persist a failed-run realization whose generated input transitions and
2377    /// directed terminal outboxes have already been staged in `inner` by the
2378    /// shared `DriverEntry` owner. Keeping this persistence step after the
2379    /// shared realization makes the queued/abandoned split and its exact
2380    /// terminal recipient batch one atomic store commit.
2381    pub(crate) async fn persist_machine_realized_run_failed(
2382        &mut self,
2383        realization: crate::meerkat_machine::driver::MachineRunFailureRealization,
2384    ) -> Result<Option<PreparedRuntimeSessionCommitResult>, RuntimeDriverError> {
2385        let crate::meerkat_machine::driver::MachineRunFailureRealization {
2386            run_id,
2387            contributing_input_ids,
2388            replay_plan,
2389            terminal_error,
2390            runtime_apply_failure,
2391            recoverable,
2392            applied_commit,
2393        } = realization;
2394        self.require_durability_ready()?;
2395        let terminal_input_ids = self
2396            .inner
2397            .archivable_terminal_input_ids_in(&contributing_input_ids)?;
2398        let checkpoint = self.persistence_rollback_checkpoint();
2399        let failure_cause = runtime_apply_failure.as_ref().map(|failure| failure.kind);
2400        tracing::debug!(
2401            run_id = ?run_id,
2402            contributors = contributing_input_ids.len(),
2403            replay_kind = replay_plan.notice_kind,
2404            recoverable,
2405            error = terminal_error,
2406            failure_cause = ?failure_cause,
2407            "persistent driver realized machine-owned failed-run replay"
2408        );
2409        let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
2410            checkpoint,
2411            &contributing_input_ids,
2412            "failed-run terminal event",
2413        )?;
2414        let persist_result = if let Some(applied_commit) = applied_commit {
2415            let request = self.prepare_machine_terminal_boundary(
2416                applied_commit.session,
2417                applied_commit.receipt,
2418                commit,
2419                input_states,
2420                applied_commit.owner_session_id,
2421            );
2422            match request {
2423                Ok(request) => self
2424                    .store
2425                    .commit_prepared_session_boundary(&self.runtime_id, request)
2426                    .await
2427                    .map(Some),
2428                Err(error) => Err(error),
2429            }
2430        } else {
2431            self.store
2432                .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
2433                .await
2434                .map(|()| None)
2435        };
2436        match persist_result {
2437            Ok(result) => {
2438                if let Err(error) = self
2439                    .inner
2440                    .archive_archivable_terminal_inputs_after_durable_commit(&terminal_input_ids)
2441                {
2442                    return Err(self.post_transition_failure(
2443                        None,
2444                        "failed_run_terminal_archive",
2445                        error.to_string(),
2446                    ));
2447                }
2448                Ok(result)
2449            }
2450            Err(err) => Err(self.post_transition_failure(
2451                checkpoint,
2452                "failed_run_terminal_commit",
2453                format!("terminal event persist failed: {err}"),
2454            )),
2455        }
2456    }
2457
2458    pub(crate) async fn machine_realize_run_cancelled(
2459        &mut self,
2460        run_id: &RunId,
2461        contributing_input_ids: &[InputId],
2462    ) -> Result<(), RuntimeDriverError> {
2463        self.require_durability_ready()?;
2464        let checkpoint = self.persistence_rollback_checkpoint();
2465        if let Err(err) = self
2466            .inner
2467            .machine_realize_run_cancelled(run_id, contributing_input_ids)
2468        {
2469            return Err(self.post_transition_failure(
2470                checkpoint,
2471                "cancelled_run_realization",
2472                err.to_string(),
2473            ));
2474        }
2475        tracing::debug!(
2476            run_id = ?run_id,
2477            contributors = contributing_input_ids.len(),
2478            "persistent driver realized machine-owned cancelled run"
2479        );
2480        let (checkpoint, input_states, commit) = self.lifecycle_persistence_payload_with_rollback(
2481            checkpoint,
2482            contributing_input_ids,
2483            "cancelled-run terminal event",
2484        )?;
2485        if let Err(err) = self
2486            .store
2487            .commit_machine_lifecycle(&self.runtime_id, commit, &input_states)
2488            .await
2489        {
2490            return Err(self.post_transition_failure(
2491                checkpoint,
2492                "cancelled_run_terminal_commit",
2493                format!("terminal cancellation persist failed: {err}"),
2494            ));
2495        }
2496        if let Err(error) = self
2497            .inner
2498            .archive_archivable_terminal_inputs_after_durable_commit(contributing_input_ids)
2499        {
2500            return Err(self.post_transition_failure(
2501                None,
2502                "cancelled_run_terminal_archive",
2503                error.to_string(),
2504            ));
2505        }
2506        Ok(())
2507    }
2508}
2509
2510#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2511#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2512impl RuntimeDriver for PersistentRuntimeDriver {
2513    async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError> {
2514        let resolved = self.resolve_admission(&input)?;
2515        self.accept_resolved_input(input, resolved).await
2516    }
2517
2518    async fn on_runtime_event(
2519        &mut self,
2520        event: RuntimeEventEnvelope,
2521    ) -> Result<(), RuntimeDriverError> {
2522        self.require_durability_ready()?;
2523        self.inner.on_runtime_event(event).await
2524    }
2525
2526    async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError> {
2527        Err(RuntimeDriverError::RecoveryRepairBlocked {
2528            evidence_digest: None,
2529            reason: "persistent driver recovery requires registration-authorized lifecycle \
2530                     convergence and an exact store-owned input-set revision; direct compatibility \
2531                     recovery is no longer supported"
2532                .to_string(),
2533        })
2534    }
2535
2536    fn runtime_state(&self) -> RuntimeState {
2537        self.inner.runtime_state()
2538    }
2539
2540    fn input_state(&self, input_id: &InputId) -> Option<&InputState> {
2541        self.inner.input_state(input_id)
2542    }
2543
2544    fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState> {
2545        self.inner.input_phase(input_id)
2546    }
2547
2548    fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId> {
2549        self.inner.input_last_run_id(input_id)
2550    }
2551
2552    fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64> {
2553        self.inner.input_last_boundary_sequence(input_id)
2554    }
2555
2556    fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState> {
2557        self.inner.stored_input_state(input_id)
2558    }
2559
2560    fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError> {
2561        self.inner.stored_input_states_snapshot()
2562    }
2563
2564    fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId> {
2565        self.inner.input_id_for_idempotency_key(idempotency_key)
2566    }
2567
2568    fn active_input_ids(&self) -> Vec<InputId> {
2569        self.inner.active_input_ids()
2570    }
2571}
2572
2573#[cfg(test)]
2574#[allow(clippy::unwrap_used, clippy::expect_used)]
2575mod tests {
2576    use super::*;
2577    use chrono::Utc;
2578    use meerkat_core::lifecycle::InputId;
2579    use meerkat_core::types::SessionId;
2580
2581    fn make_prompt(text: &str) -> Input {
2582        Input::Prompt(crate::input::PromptInput {
2583            injected_context: Vec::new(),
2584            header: crate::input::InputHeader {
2585                id: InputId::new(),
2586                timestamp: Utc::now(),
2587                source: crate::input::InputOrigin::Operator,
2588                durability: crate::input::InputDurability::Durable,
2589                visibility: crate::input::InputVisibility::default(),
2590                idempotency_key: None,
2591                supersession_key: None,
2592                correlation_id: None,
2593            },
2594            content: text.into(),
2595            typed_turn_appends: Vec::new(),
2596            turn_metadata: None,
2597        })
2598    }
2599
2600    async fn recover_after_registration_authority(
2601        store: &crate::store::InMemoryRuntimeStore,
2602        session_id: &SessionId,
2603        driver: &mut PersistentRuntimeDriver,
2604    ) -> RecoveryReport {
2605        let recovery =
2606            crate::meerkat_machine::driver::reconcile_runtime_authority_for_cold_recovery(
2607                store,
2608                &driver.runtime_id,
2609                session_id,
2610            )
2611            .await
2612            .expect("registration must converge durable runtime authority");
2613        driver
2614            .inner_mut()
2615            .replace_runtime_authority(recovery.authority);
2616        driver
2617            .recover_inputs_after_runtime_authority(recovery.unregister_progress.as_ref())
2618            .await
2619            .expect("registration-authorized input recovery must commit by exact batch CAS")
2620    }
2621
2622    #[test]
2623    fn provisional_promotion_is_bound_to_run_session_and_store_profile() {
2624        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
2625        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
2626        let driver = PersistentRuntimeDriver::new(
2627            LogicalRuntimeId::new("provisional-promotion-profile"),
2628            store,
2629            blob_store,
2630        );
2631        let session_id = meerkat_core::Session::new().id().clone();
2632        let run_id = RunId::new();
2633        let receipt = RunBoundaryReceipt {
2634            run_id: run_id.clone(),
2635            boundary: meerkat_core::lifecycle::run_primitive::RunApplyBoundary::Immediate,
2636            contributing_input_ids: Vec::new(),
2637            conversation_digest: Some("checkpoint-digest".to_string()),
2638            message_count: 1,
2639            sequence: 1,
2640        };
2641        let whole_blob = meerkat_core::RunCheckpointReceipt::issued(
2642            meerkat_core::RunCheckpointAuthority::WholeBlob(
2643                meerkat_core::WholeBlobProvisionalTailAuthority::issued(
2644                    session_id.clone(),
2645                    4,
2646                    "row-sha256:base".to_string(),
2647                    run_id.clone(),
2648                    "row-sha256:candidate".to_string(),
2649                    1,
2650                )
2651                .unwrap(),
2652            ),
2653            "checkpoint-digest".to_string(),
2654            1,
2655        )
2656        .unwrap();
2657        assert!(matches!(
2658            driver
2659                .prepare_provisional_promotion(&whole_blob, &receipt, &session_id)
2660                .unwrap(),
2661            PreparedProvisionalPromotion::WholeBlob(_)
2662        ));
2663
2664        let wrong_run_receipt = RunBoundaryReceipt {
2665            run_id: RunId::new(),
2666            ..receipt.clone()
2667        };
2668        assert!(matches!(
2669            driver.prepare_provisional_promotion(&whole_blob, &wrong_run_receipt, &session_id),
2670            Err(RuntimeStoreError::SessionPersistenceAuthorityConflict { .. })
2671        ));
2672
2673        let head_canonical = meerkat_core::RunCheckpointReceipt::issued(
2674            meerkat_core::RunCheckpointAuthority::HeadCanonical(
2675                meerkat_core::HeadCanonicalProvisionalTailAuthority::issued(
2676                    session_id.clone(),
2677                    4,
2678                    "head:base".to_string(),
2679                    5,
2680                    "head:candidate".to_string(),
2681                    run_id,
2682                    1,
2683                )
2684                .unwrap(),
2685            ),
2686            "checkpoint-digest".to_string(),
2687            1,
2688        )
2689        .unwrap();
2690        assert!(matches!(
2691            driver.prepare_provisional_promotion(&head_canonical, &receipt, &session_id),
2692            Err(RuntimeStoreError::SessionPersistenceAuthorityConflict { .. })
2693        ));
2694    }
2695
2696    /// Dogma K11 (Persistent destroy / driver-side shadow truth): every
2697    /// fallible step of `commit_lifecycle_with_rollback` AFTER the caller has
2698    /// staged a DSL lifecycle transition must restore the caller's checkpoint.
2699    /// The input-state snapshot read used to escape with a bare `?`, leaving
2700    /// the staged lifecycle live in driver state while reporting failure.
2701    #[tokio::test]
2702    async fn commit_lifecycle_snapshot_failure_restores_checkpoint() {
2703        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
2704        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
2705        let rid = LogicalRuntimeId::new("commit-lifecycle-rollback-contract");
2706        let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
2707
2708        // Checkpoint BEFORE any state mutation (the caller's pre-stage view).
2709        let checkpoint = driver.inner.rollback_snapshot();
2710
2711        // Mutate driver state past the checkpoint (stands in for a staged
2712        // Destroy/lifecycle transition awaiting durable commit).
2713        let input = make_prompt("staged work");
2714        let input_id = input.id().clone();
2715        let outcome = driver.accept_input(input).await.unwrap();
2716        assert!(outcome.is_accepted());
2717        assert!(driver.input_phase(&input_id).is_some());
2718
2719        // Inject a failure into the input-state snapshot step.
2720        driver.force_input_snapshot_failure_for_test = true;
2721        let target_state = driver.inner_ref().runtime_state();
2722        let result = driver
2723            .commit_lifecycle_with_rollback(Some(checkpoint), &[], target_state, "test destroy")
2724            .await;
2725
2726        // The failure must propagate typed AND the staged driver state must be
2727        // rolled back to the checkpoint — no half-destroyed shadow truth.
2728        assert!(result.is_err(), "forced snapshot failure must propagate");
2729        assert!(
2730            driver.input_phase(&input_id).is_none(),
2731            "staged driver state must be restored to the pre-stage checkpoint"
2732        );
2733        assert!(driver.active_input_ids().is_empty());
2734    }
2735
2736    /// Same K11 checkpoint-restore contract for `abandon_pending_inputs`: the
2737    /// input-state snapshot / lifecycle-commit classification steps between
2738    /// the staged `&mut` abandon and the durable commit used to escape with a
2739    /// bare `?`, leaving the abandon applied in memory while reporting
2740    /// failure (and never persisting it).
2741    #[tokio::test]
2742    async fn abandon_pending_inputs_snapshot_failure_restores_checkpoint() {
2743        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
2744        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
2745        let rid = LogicalRuntimeId::new("abandon-rollback-contract");
2746        let mut driver = PersistentRuntimeDriver::new(rid, store, blob_store);
2747
2748        // Accept a pending input so the abandon has staged work to mutate.
2749        let input = make_prompt("pending work");
2750        let input_id = input.id().clone();
2751        let outcome = driver.accept_input(input).await.unwrap();
2752        assert!(outcome.is_accepted());
2753        assert!(driver.input_phase(&input_id).is_some());
2754
2755        // Inject a failure into the input-state snapshot step that runs after
2756        // the staged abandon mutation.
2757        driver.force_input_snapshot_failure_for_test = true;
2758        let result = driver
2759            .abandon_pending_inputs(InputAbandonReason::Reset)
2760            .await;
2761
2762        assert!(result.is_err(), "forced snapshot failure must propagate");
2763        assert!(
2764            driver.input_phase(&input_id).is_some(),
2765            "staged abandon must be rolled back: the pending input must still be live"
2766        );
2767    }
2768
2769    #[tokio::test]
2770    async fn retiring_active_run_persists_retired_before_dropping_live_witness() {
2771        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
2772        let runtime_id = LogicalRuntimeId::new("retire-active-run-durability");
2773        let runtime_store: Arc<dyn RuntimeStore> = store.clone();
2774        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
2775        let mut driver =
2776            PersistentRuntimeDriver::new(runtime_id.clone(), runtime_store, blob_store);
2777        let run_id = RunId::new();
2778
2779        driver
2780            .contract_begin_run_authority(run_id.clone())
2781            .expect("contract run admission");
2782        assert_eq!(driver.runtime_state(), RuntimeState::Running);
2783        assert_eq!(driver.inner_ref().current_run_id(), Some(run_id));
2784        assert!(driver.inner_ref().pre_run_phase().is_some());
2785
2786        let session_id = driver.inner_ref().session_authority_id_for_recovery();
2787        {
2788            let authority = driver.inner_ref().shared_dsl_authority();
2789            let mut authority = authority
2790                .lock()
2791                .unwrap_or_else(std::sync::PoisonError::into_inner);
2792            crate::meerkat_machine::dsl::MeerkatMachineMutator::apply(
2793                &mut *authority,
2794                crate::meerkat_machine::dsl::MeerkatMachineInput::Retire { session_id },
2795            )
2796            .expect("machine-authorized mid-run retire transition");
2797        }
2798        driver.sync_control_projection_from_dsl_authority();
2799        assert_eq!(driver.runtime_state(), RuntimeState::Retired);
2800        assert!(
2801            driver.inner_ref().pre_run_phase().is_some(),
2802            "Retire commits before the live run witness is dropped"
2803        );
2804
2805        driver
2806            .realize_retire_lifecycle()
2807            .await
2808            .expect("mid-run retire must durably commit");
2809
2810        assert_eq!(driver.runtime_state(), RuntimeState::Retired);
2811        assert_eq!(
2812            crate::store::load_runtime_state(store.as_ref(), &runtime_id)
2813                .await
2814                .expect("reload durable lifecycle"),
2815            Some(RuntimeState::Retired)
2816        );
2817    }
2818
2819    #[tokio::test]
2820    async fn interaction_terminal_outbox_delegator_swaps_exact_rows_and_reports_stale() {
2821        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
2822        let store_trait: Arc<dyn RuntimeStore> = store.clone();
2823        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
2824        let rid = LogicalRuntimeId::new("interaction-outbox-cas-delegator");
2825        let mut driver = PersistentRuntimeDriver::new(rid.clone(), store_trait, blob_store);
2826
2827        let mut input_ids = Vec::new();
2828        for text in ["first", "second"] {
2829            let input = make_prompt(text);
2830            input_ids.push(input.id().clone());
2831            assert!(driver.accept_input(input).await.unwrap().is_accepted());
2832        }
2833        // The persistent accept path intentionally previews and durably
2834        // commits an isolated staged driver before realizing the same
2835        // admission in the live driver.  Capture the CAS witness from the
2836        // durable store, as recovery adoption does, instead of assuming the
2837        // two independently timestamped admission shells are byte-identical.
2838        let expected = store.load_input_states_strict(&rid).await.unwrap();
2839        for input_id in &input_ids {
2840            driver
2841                .inner_mut()
2842                .ledger_mut()
2843                .get_mut(input_id)
2844                .unwrap()
2845                .recovery_count = 1;
2846        }
2847
2848        assert_eq!(
2849            driver
2850                .compare_and_swap_interaction_terminal_outbox_inputs(&expected, &input_ids)
2851                .await
2852                .unwrap(),
2853            InputStateBatchCasOutcome::Swapped
2854        );
2855        assert!(
2856            store
2857                .load_input_states_strict(&rid)
2858                .await
2859                .unwrap()
2860                .iter()
2861                .all(|row| row.state.recovery_count == 1)
2862        );
2863
2864        for input_id in &input_ids {
2865            driver
2866                .inner_mut()
2867                .ledger_mut()
2868                .get_mut(input_id)
2869                .unwrap()
2870                .recovery_count = 2;
2871        }
2872        assert_eq!(
2873            driver
2874                .compare_and_swap_interaction_terminal_outbox_inputs(&expected, &input_ids)
2875                .await
2876                .unwrap(),
2877            InputStateBatchCasOutcome::Stale
2878        );
2879        assert!(
2880            store
2881                .load_input_states_strict(&rid)
2882                .await
2883                .unwrap()
2884                .iter()
2885                .all(|row| row.state.recovery_count == 1),
2886            "a stale delegator CAS must not mutate any durable row"
2887        );
2888    }
2889
2890    #[tokio::test]
2891    async fn recover_atomically_rewrites_cold_running_lifecycle_to_idle() {
2892        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
2893        let session_id = SessionId::new();
2894        let runtime_id = LogicalRuntimeId::for_session(&session_id);
2895        store
2896            .commit_machine_lifecycle(
2897                &runtime_id,
2898                MachineLifecycleCommit::new_with_binding(
2899                    RuntimeState::Running,
2900                    crate::store::MachineLifecycleBindingFacts::new(
2901                        Some("rt:cold-running".to_string()),
2902                        Some(9),
2903                        Some(2),
2904                        Some("epoch-cold-running".to_string()),
2905                    ),
2906                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
2907                ),
2908                &[],
2909            )
2910            .await
2911            .expect("seed torn cold Running lifecycle");
2912
2913        let runtime_store: Arc<dyn RuntimeStore> = store.clone();
2914        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
2915        let mut driver =
2916            PersistentRuntimeDriver::new(runtime_id.clone(), runtime_store, blob_store);
2917
2918        recover_after_registration_authority(store.as_ref(), &session_id, &mut driver).await;
2919
2920        assert_eq!(driver.runtime_state(), RuntimeState::Idle);
2921        assert_eq!(
2922            crate::store::load_runtime_state(store.as_ref(), &runtime_id)
2923                .await
2924                .expect("reload durable lifecycle"),
2925            Some(RuntimeState::Idle),
2926            "recovery acknowledgement must mean the torn lifecycle row is repaired"
2927        );
2928    }
2929
2930    #[tokio::test]
2931    async fn exact_batch_cas_fences_stale_two_handle_finalization_and_publication_writes() {
2932        let store = Arc::new(crate::store::InMemoryRuntimeStore::new());
2933        let store_trait: Arc<dyn RuntimeStore> = store.clone();
2934        let blob_store: Arc<dyn BlobStore> = Arc::new(meerkat_store::MemoryBlobStore::new());
2935        let session_id = SessionId::new();
2936        let rid = LogicalRuntimeId::for_session(&session_id);
2937        let mut owner =
2938            PersistentRuntimeDriver::new(rid.clone(), store_trait.clone(), blob_store.clone());
2939        recover_after_registration_authority(store.as_ref(), &session_id, &mut owner).await;
2940        let mut input_ids = Vec::new();
2941        for text in ["first", "second"] {
2942            let input = make_prompt(text);
2943            input_ids.push(input.id().clone());
2944            assert!(owner.accept_input(input).await.unwrap().is_accepted());
2945        }
2946
2947        // First owner acquires the durable batch witness.
2948        let initial = store.load_input_states_strict(&rid).await.unwrap();
2949        for input_id in &input_ids {
2950            owner
2951                .inner_mut()
2952                .ledger_mut()
2953                .get_mut(input_id)
2954                .unwrap()
2955                .recovery_count = 10;
2956        }
2957        assert_eq!(
2958            owner
2959                .compare_and_swap_interaction_terminal_outbox_inputs(&initial, &input_ids)
2960                .await
2961                .unwrap(),
2962            InputStateBatchCasOutcome::Swapped
2963        );
2964        let owner_witness = store.load_input_states_strict(&rid).await.unwrap();
2965
2966        // A second store handle takes over before Candidate -> Finalized.
2967        let mut takeover =
2968            PersistentRuntimeDriver::new(rid.clone(), store_trait.clone(), blob_store.clone());
2969        recover_after_registration_authority(store.as_ref(), &session_id, &mut takeover).await;
2970        let takeover_expected = store.load_input_states_strict(&rid).await.unwrap();
2971        for input_id in &input_ids {
2972            takeover
2973                .inner_mut()
2974                .ledger_mut()
2975                .get_mut(input_id)
2976                .unwrap()
2977                .recovery_count = 20;
2978        }
2979        assert_eq!(
2980            takeover
2981                .compare_and_swap_interaction_terminal_outbox_inputs(
2982                    &takeover_expected,
2983                    &input_ids,
2984                )
2985                .await
2986                .unwrap(),
2987            InputStateBatchCasOutcome::Swapped
2988        );
2989        for input_id in &input_ids {
2990            owner
2991                .inner_mut()
2992                .ledger_mut()
2993                .get_mut(input_id)
2994                .unwrap()
2995                .recovery_count = 30;
2996        }
2997        assert_eq!(
2998            owner
2999                .compare_and_swap_interaction_terminal_outbox_inputs(&owner_witness, &input_ids)
3000                .await
3001                .unwrap(),
3002            InputStateBatchCasOutcome::Stale,
3003            "the superseded owner must not overwrite takeover at finalization"
3004        );
3005        assert!(
3006            store
3007                .load_input_states_strict(&rid)
3008                .await
3009                .unwrap()
3010                .iter()
3011                .all(|row| row.state.recovery_count == 20)
3012        );
3013
3014        // The takeover owner finalizes, then a third handle takes ownership
3015        // before Finalized -> Published. The old finalizer's receipt write is
3016        // fenced by its exact pre-publication witness.
3017        let takeover_witness = store.load_input_states_strict(&rid).await.unwrap();
3018        for input_id in &input_ids {
3019            takeover
3020                .inner_mut()
3021                .ledger_mut()
3022                .get_mut(input_id)
3023                .unwrap()
3024                .recovery_count = 40;
3025        }
3026        assert_eq!(
3027            takeover
3028                .compare_and_swap_interaction_terminal_outbox_inputs(&takeover_witness, &input_ids,)
3029                .await
3030                .unwrap(),
3031            InputStateBatchCasOutcome::Swapped
3032        );
3033        let finalized_witness = store.load_input_states_strict(&rid).await.unwrap();
3034        let mut publisher = PersistentRuntimeDriver::new(rid.clone(), store_trait, blob_store);
3035        recover_after_registration_authority(store.as_ref(), &session_id, &mut publisher).await;
3036        let publisher_expected = store.load_input_states_strict(&rid).await.unwrap();
3037        for input_id in &input_ids {
3038            publisher
3039                .inner_mut()
3040                .ledger_mut()
3041                .get_mut(input_id)
3042                .unwrap()
3043                .recovery_count = 50;
3044        }
3045        assert_eq!(
3046            publisher
3047                .compare_and_swap_interaction_terminal_outbox_inputs(
3048                    &publisher_expected,
3049                    &input_ids,
3050                )
3051                .await
3052                .unwrap(),
3053            InputStateBatchCasOutcome::Swapped
3054        );
3055        for input_id in &input_ids {
3056            takeover
3057                .inner_mut()
3058                .ledger_mut()
3059                .get_mut(input_id)
3060                .unwrap()
3061                .recovery_count = 60;
3062        }
3063        assert_eq!(
3064            takeover
3065                .compare_and_swap_interaction_terminal_outbox_inputs(
3066                    &finalized_witness,
3067                    &input_ids,
3068                )
3069                .await
3070                .unwrap(),
3071            InputStateBatchCasOutcome::Stale,
3072            "the superseded finalizer must not overwrite takeover at publication"
3073        );
3074        assert!(
3075            store
3076                .load_input_states_strict(&rid)
3077                .await
3078                .unwrap()
3079                .iter()
3080                .all(|row| row.state.recovery_count == 50)
3081        );
3082    }
3083}