1use 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
36pub struct PersistentRuntimeDriver {
38 inner: EphemeralRuntimeDriver,
40 store: Arc<dyn RuntimeStore>,
42 blob_store: Arc<dyn BlobStore>,
44 runtime_id: LogicalRuntimeId,
46 durability_health: Option<crate::meerkat_machine::DurabilityHealthHandle>,
50 input_state_write_fence: Option<Arc<dyn RuntimeStoreWriteFence>>,
56 #[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 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 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 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 pub(crate) fn durability_health_handle(
535 &self,
536 ) -> Option<crate::meerkat_machine::DurabilityHealthHandle> {
537 self.durability_health.clone()
538 }
539
540 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 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 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 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 pub fn is_idle(&self) -> bool {
1042 self.inner.is_idle()
1043 }
1044
1045 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 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 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 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 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 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 RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
1503 reason: format!(
1504 "{context} retained lifecycle finalization acknowledgement unavailable: {error}"
1505 ),
1506 }
1507 })
1508 }
1509
1510 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 #[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 pub fn drain_events(&mut self) -> Vec<RuntimeEventEnvelope> {
1559 self.inner.drain_events()
1560 }
1561
1562 pub fn take_post_admission_signal(&mut self) -> crate::driver::ephemeral::PostAdmissionSignal {
1564 self.inner.take_post_admission_signal()
1565 }
1566
1567 pub fn post_admission_signal(&self) -> crate::driver::ephemeral::PostAdmissionSignal {
1569 self.inner.post_admission_signal()
1570 }
1571
1572 pub fn take_wake_requested(&mut self) -> bool {
1574 self.inner.take_wake_requested()
1575 }
1576
1577 pub fn take_process_requested(&mut self) -> bool {
1579 self.inner.take_process_requested()
1580 }
1581
1582 #[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 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 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 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 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 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 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 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 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 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 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 #[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 let checkpoint = driver.inner.rollback_snapshot();
2710
2711 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 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 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 #[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 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 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 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 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 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 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}