Skip to main content

meerkat_runtime/store/
mod.rs

1//! RuntimeStore — atomic persistence for runtime state.
2//!
3//! Machine-owned runtime commands durably persist [`RunBoundaryReceipt`] values
4//! atomically with their session and input-state effects.
5
6pub mod memory;
7#[cfg(feature = "sqlite-store")]
8pub mod sqlite;
9
10use std::collections::{HashMap, HashSet};
11
12use meerkat_core::lifecycle::{InputId, RunBoundaryReceipt, RunId};
13use sha2::{Digest, Sha256};
14
15use crate::identifiers::LogicalRuntimeId;
16use crate::input_state::{InputStatePersistenceRecord, StoredInputState};
17use crate::runtime_state::RuntimeState;
18
19const LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 1;
20const SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 2;
21const UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 3;
22pub(crate) const MACHINE_LIFECYCLE_STORE_RECORD_VERSION: u16 = 4;
23
24/// Maximum number of exact input-state rows admitted by one compare-and-swap
25/// boundary. Directed-terminal outbox batches share the same 256-row bound as
26/// their publication seam.
27pub const MAX_INPUT_STATE_BATCH_CAS: usize = 256;
28
29/// Result of an exact input-state batch compare-and-swap.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum InputStateBatchCasOutcome {
32    /// Every expected durable row matched and every replacement committed, or
33    /// every durable row was already byte-identical to its replacement from an
34    /// earlier invocation whose acknowledgement was lost.
35    Swapped,
36    /// At least one expected row was missing or no longer byte-identical; no
37    /// replacement was written.
38    Stale,
39}
40
41/// Result of an exact input-state batch compare-and-swap performed under an
42/// external authority fence.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum FencedInputStateBatchCasOutcome {
45    /// The target rows matched and the replacements committed, or were already
46    /// byte-identical, while the external authority was current.
47    Swapped,
48    /// At least one target row no longer matched. The external fence was not
49    /// consulted and no replacement was written.
50    Stale,
51    /// The external authority was superseded. No replacement was written.
52    FenceConflict { reason: String },
53    /// The external authority could not be checked temporarily. No replacement
54    /// was written.
55    FenceBackoff { reason: String },
56}
57
58#[derive(Debug)]
59struct PreparedInputStateBatchCasRow {
60    input_id: InputId,
61    expected_json: Vec<u8>,
62    replacement: StoredInputState,
63    // SQLite writes the already-validated bytes; the in-memory implementation
64    // stores the typed replacement directly.
65    #[cfg_attr(not(feature = "sqlite-store"), allow(dead_code))]
66    replacement_json: Vec<u8>,
67}
68
69fn prepare_input_state_batch_cas(
70    expected: &[StoredInputState],
71    replacements: &[InputStatePersistenceRecord],
72) -> Result<Vec<PreparedInputStateBatchCasRow>, RuntimeStoreError> {
73    if expected.len() != replacements.len() {
74        return Err(RuntimeStoreError::InvalidInputStateBatchCas {
75            reason: format!(
76                "expected row count {} does not match replacement row count {}",
77                expected.len(),
78                replacements.len()
79            ),
80        });
81    }
82    if expected.len() > MAX_INPUT_STATE_BATCH_CAS {
83        return Err(RuntimeStoreError::InvalidInputStateBatchCas {
84            reason: format!(
85                "batch contains {} rows, exceeding the maximum of {MAX_INPUT_STATE_BATCH_CAS}",
86                expected.len()
87            ),
88        });
89    }
90
91    let mut expected_ids = HashSet::with_capacity(expected.len());
92    for row in expected {
93        if !expected_ids.insert(row.state.input_id.clone()) {
94            return Err(RuntimeStoreError::InvalidInputStateBatchCas {
95                reason: format!("expected batch repeats input {}", row.state.input_id),
96            });
97        }
98    }
99
100    let mut replacement_by_id = HashMap::with_capacity(replacements.len());
101    for record in replacements {
102        let replacement = record.clone_stored();
103        let input_id = replacement.state.input_id.clone();
104        let replacement_json = serde_json::to_vec(&replacement)
105            .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
106        if replacement_by_id
107            .insert(input_id.clone(), (replacement, replacement_json))
108            .is_some()
109        {
110            return Err(RuntimeStoreError::InvalidInputStateBatchCas {
111                reason: format!("replacement batch repeats input {input_id}"),
112            });
113        }
114    }
115
116    let mut prepared = Vec::with_capacity(expected.len());
117    for expected_row in expected {
118        let input_id = expected_row.state.input_id.clone();
119        let Some((replacement, replacement_json)) = replacement_by_id.remove(&input_id) else {
120            return Err(RuntimeStoreError::InvalidInputStateBatchCas {
121                reason: format!("replacement batch does not contain expected input {input_id}"),
122            });
123        };
124        let expected_json = serde_json::to_vec(expected_row)
125            .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
126        prepared.push(PreparedInputStateBatchCasRow {
127            input_id,
128            expected_json,
129            replacement,
130            replacement_json,
131        });
132    }
133    if let Some(extra) = replacement_by_id.keys().next() {
134        return Err(RuntimeStoreError::InvalidInputStateBatchCas {
135            reason: format!("replacement batch contains unexpected input {extra}"),
136        });
137    }
138    Ok(prepared)
139}
140
141/// Errors from RuntimeStore operations.
142#[derive(Debug, Clone, thiserror::Error)]
143#[non_exhaustive]
144pub enum RuntimeStoreError {
145    /// Write failed.
146    #[error("Store write failed: {0}")]
147    WriteFailed(String),
148    /// Read failed.
149    #[error("Store read failed: {0}")]
150    ReadFailed(String),
151    /// The explicit session-store key does not match the serialized session.
152    #[error("Session store key mismatch: expected {expected}, actual {actual}")]
153    SessionKeyMismatch {
154        expected: meerkat_core::types::SessionId,
155        actual: meerkat_core::types::SessionId,
156    },
157    /// Not found.
158    #[error("Not found: {0}")]
159    NotFound(String),
160    /// Operation is not supported by this store implementation.
161    #[error("Unsupported store operation: {0}")]
162    Unsupported(String),
163    /// A detached producer attempted to persist an ops snapshot after the
164    /// matching epoch was atomically retired by unregister.
165    #[error("Ops lifecycle epoch {epoch_id} for runtime {runtime_id} is retired")]
166    OpsLifecycleEpochRetired {
167        runtime_id: String,
168        epoch_id: meerkat_core::RuntimeEpochId,
169    },
170    /// An unregister-finalization commit may have become durable, but the
171    /// backend could not authoritatively classify its outcome.
172    ///
173    /// Callers must retry the idempotent atomic finalization and must not
174    /// publish a compensating lifecycle rollback for this error.
175    #[error("Unregister finalization outcome is unknown: {0}")]
176    UnregisterFinalizationOutcomeUnknown(String),
177    /// Runtime snapshot CAS rejected a stale transcript rewrite.
178    #[error("Transcript revision conflict: expected {expected}, actual {actual}")]
179    TranscriptRevisionConflict { expected: String, actual: String },
180    /// An atomic boundary commit carried a session snapshot that was already
181    /// superseded by the durable append-only head. Callers must observe this
182    /// as a failed commit rather than mistaking a no-op for publication.
183    #[error("Session snapshot for runtime '{runtime_id}' was superseded by the durable head")]
184    SessionSnapshotSuperseded { runtime_id: String },
185    /// The requested exact input-state batch CAS has an invalid row/key shape.
186    #[error("Invalid input-state batch compare-and-swap: {reason}")]
187    InvalidInputStateBatchCas { reason: String },
188    /// A lifecycle record was observed exactly, but replacing it would risk
189    /// lowering or fabricating durable runtime fencing authority.
190    ///
191    /// This is a permanent reconciliation result for the observed row, not a
192    /// transport retry. Callers should project RepairBlocked while retaining
193    /// the evidence digest for operator repair.
194    #[error("Machine lifecycle repair is blocked: {detail}")]
195    MachineLifecycleRepairBlocked {
196        evidence_digest: Option<String>,
197        detail: String,
198    },
199    /// Internal error.
200    #[error("Internal error: {0}")]
201    Internal(String),
202}
203
204/// Transactional updater for the runtime-owned OAuth login-flow payload snapshot.
205pub type AuthOAuthFlowSnapshotUpdate<'a> =
206    dyn FnMut(Option<&[u8]>) -> Result<Vec<u8>, RuntimeStoreError> + 'a;
207
208/// Describes a serialized session snapshot for boundary and snapshot-only commits.
209#[derive(Debug, Clone)]
210pub struct SessionDelta {
211    /// Serialized session snapshot (opaque to RuntimeStore).
212    pub session_snapshot: Vec<u8>,
213}
214
215fn validated_compaction_projection_intents(
216    session: &meerkat_core::Session,
217) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeStoreError> {
218    session
219        .validated_compaction_projection_intents()
220        .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))
221}
222
223/// Clear one finalized compaction intent while preserving typed checkpoint
224/// custody for the exact replacement document.
225///
226/// Legacy snapshots retain their legacy-unverified classification; callers do
227/// not gain typed authority merely by finalizing an outbox row. A verified
228/// snapshot advances by one exact run-boundary successor so the persisted
229/// document can never carry its predecessor's now-stale digest.
230pub(crate) fn complete_compaction_projection_checkpoint(
231    session: &mut meerkat_core::Session,
232    projection: &meerkat_core::CompactionProjectionId,
233) -> Result<(), RuntimeStoreError> {
234    let predecessor = match session
235        .try_checkpoint_state()
236        .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?
237    {
238        meerkat_core::SessionCheckpointState::Verified(stamp) => Some(stamp),
239        meerkat_core::SessionCheckpointState::LegacyUnverified { .. } => None,
240    };
241
242    let completed = session
243        .complete_compaction_projection_intent(projection)
244        .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
245
246    if completed.is_none() {
247        return Ok(());
248    }
249
250    if let Some(predecessor) = predecessor {
251        let successor = meerkat_core::SessionCheckpointStamp::successor(
252            session,
253            &predecessor,
254            meerkat_core::SessionCheckpointProvenance::RunBoundaryCommit,
255        )
256        .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
257        session
258            .install_checkpoint_stamp(successor)
259            .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
260    }
261
262    Ok(())
263}
264
265/// Runtime binding facts selected by generated MeerkatMachine authority.
266///
267/// RuntimeStore implementations persist and read these facts as part of a
268/// machine lifecycle snapshot. The commit token that writes these facts stays
269/// crate-private so compatibility callers cannot mint replacement lifecycle
270/// truth.
271#[derive(Debug, Clone, Default, PartialEq, Eq)]
272pub struct MachineLifecycleBindingFacts {
273    agent_runtime_id: Option<String>,
274    fence_token: Option<u64>,
275    runtime_generation: Option<u64>,
276    runtime_epoch_id: Option<String>,
277}
278
279/// Durable identity receipt for the last completed supervisor revoke.
280///
281/// This is not a live supervisor binding and carries no route address. It is
282/// only the identity/key/epoch witness needed to authorize an exact duplicate
283/// revoke response after a cold restart; the current authenticated request
284/// supplies its current route.
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct RevokedSupervisorReceipt {
287    peer_id: String,
288    signing_public_key: String,
289    epoch: u64,
290}
291
292/// Durable current supervisor binding used to authenticate terminal retry
293/// traffic after a cold runtime restart.
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct SupervisorBindingReceipt {
296    name: String,
297    peer_id: String,
298    address: String,
299    signing_public_key: String,
300    epoch: u64,
301}
302
303/// Durable in-flight supervisor revocation receipt.
304///
305/// This is the closed-world hand-off between generated machine authority and
306/// the concrete router mutation.  It deliberately retains the complete prior
307/// route so a cold runtime can authenticate an exact retry and re-materialize
308/// the generated remove obligation without resurrecting a live binding.
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct SupervisorRevocationPendingReceipt {
311    name: String,
312    peer_id: String,
313    address: String,
314    signing_public_key: String,
315    epoch: u64,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
319#[serde(rename_all = "snake_case")]
320pub enum SupervisorRotationPersistencePhase {
321    PreviousRevokePending,
322    NextPublishPending,
323    Completed,
324    Rejected,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
328#[serde(rename_all = "snake_case")]
329pub enum SupervisorRotationRejection {
330    OperationConflict,
331    NotBound,
332    SenderMismatch,
333    TargetEpochNotAdvanced,
334    InvalidTarget,
335    UnsupportedProtocolVersion,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct SupervisorRotationReceipt {
340    operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
341    phase: SupervisorRotationPersistencePhase,
342    rejection: Option<SupervisorRotationRejection>,
343    previous: SupervisorBindingReceipt,
344    next: SupervisorBindingReceipt,
345}
346
347impl SupervisorRotationReceipt {
348    pub(crate) fn new(
349        operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
350        phase: SupervisorRotationPersistencePhase,
351        rejection: Option<SupervisorRotationRejection>,
352        previous: SupervisorBindingReceipt,
353        next: SupervisorBindingReceipt,
354    ) -> Self {
355        Self {
356            operation_id,
357            phase,
358            rejection,
359            previous,
360            next,
361        }
362    }
363
364    pub fn operation_id(
365        &self,
366    ) -> meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId {
367        self.operation_id
368    }
369
370    pub fn phase(&self) -> SupervisorRotationPersistencePhase {
371        self.phase
372    }
373
374    pub fn rejection(&self) -> Option<SupervisorRotationRejection> {
375        self.rejection
376    }
377
378    pub fn previous(&self) -> &SupervisorBindingReceipt {
379        &self.previous
380    }
381
382    pub fn next(&self) -> &SupervisorBindingReceipt {
383        &self.next
384    }
385}
386
387impl SupervisorBindingReceipt {
388    pub(crate) fn new(
389        name: String,
390        peer_id: String,
391        address: String,
392        signing_public_key: String,
393        epoch: u64,
394    ) -> Self {
395        Self {
396            name,
397            peer_id,
398            address,
399            signing_public_key,
400            epoch,
401        }
402    }
403
404    pub fn name(&self) -> &str {
405        &self.name
406    }
407
408    pub fn peer_id(&self) -> &str {
409        &self.peer_id
410    }
411
412    pub fn address(&self) -> &str {
413        &self.address
414    }
415
416    pub fn signing_public_key(&self) -> &str {
417        &self.signing_public_key
418    }
419
420    pub fn epoch(&self) -> u64 {
421        self.epoch
422    }
423}
424
425impl RevokedSupervisorReceipt {
426    pub(crate) fn new(peer_id: String, signing_public_key: String, epoch: u64) -> Self {
427        Self {
428            peer_id,
429            signing_public_key,
430            epoch,
431        }
432    }
433
434    pub fn peer_id(&self) -> &str {
435        &self.peer_id
436    }
437
438    pub fn signing_public_key(&self) -> &str {
439        &self.signing_public_key
440    }
441
442    pub fn epoch(&self) -> u64 {
443        self.epoch
444    }
445}
446
447impl SupervisorRevocationPendingReceipt {
448    pub(crate) fn new(
449        name: String,
450        peer_id: String,
451        address: String,
452        signing_public_key: String,
453        epoch: u64,
454    ) -> Self {
455        Self {
456            name,
457            peer_id,
458            address,
459            signing_public_key,
460            epoch,
461        }
462    }
463
464    pub fn name(&self) -> &str {
465        &self.name
466    }
467
468    pub fn peer_id(&self) -> &str {
469        &self.peer_id
470    }
471
472    pub fn address(&self) -> &str {
473        &self.address
474    }
475
476    pub fn signing_public_key(&self) -> &str {
477        &self.signing_public_key
478    }
479
480    pub fn epoch(&self) -> u64 {
481        self.epoch
482    }
483}
484
485/// Closed durable supervisor authority state. Each variant owns one complete
486/// recovery shape; terminal rotation receipts retain their exact operation and
487/// participant descriptors for idempotent submission and later observation.
488#[derive(Debug, Clone, Default, PartialEq, Eq)]
489pub enum SupervisorAuthoritySnapshot {
490    #[default]
491    UnboundNoReceipt,
492    Bound(SupervisorBindingReceipt),
493    RevocationPending(SupervisorRevocationPendingReceipt),
494    RotationOperation(SupervisorRotationReceipt),
495    RevokedReceipt(RevokedSupervisorReceipt),
496    WithRotationHistory {
497        current: Box<SupervisorAuthoritySnapshot>,
498        terminal_receipts: std::collections::BTreeMap<
499            meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
500            SupervisorRotationReceipt,
501        >,
502    },
503}
504
505impl MachineLifecycleBindingFacts {
506    pub(crate) fn new(
507        agent_runtime_id: Option<String>,
508        fence_token: Option<u64>,
509        runtime_generation: Option<u64>,
510        runtime_epoch_id: Option<String>,
511    ) -> Self {
512        Self {
513            agent_runtime_id,
514            fence_token,
515            runtime_generation,
516            runtime_epoch_id,
517        }
518    }
519
520    pub fn agent_runtime_id(&self) -> Option<&str> {
521        self.agent_runtime_id.as_deref()
522    }
523
524    pub fn fence_token(&self) -> Option<u64> {
525        self.fence_token
526    }
527
528    pub fn runtime_generation(&self) -> Option<u64> {
529        self.runtime_generation
530    }
531
532    pub fn runtime_epoch_id(&self) -> Option<&str> {
533        self.runtime_epoch_id.as_deref()
534    }
535}
536
537/// Exact content version of one observed machine-lifecycle row.
538///
539/// The version is the domain-prefixed SHA-256 digest of the raw stored bytes,
540/// not a decoded projection. It therefore remains a valid target-local CAS
541/// witness for unsupported and malformed rows as well as current records.
542#[derive(Debug, Clone, PartialEq, Eq, Hash)]
543pub struct MachineLifecycleObservationVersion(String);
544
545impl MachineLifecycleObservationVersion {
546    /// Derive the exact target-local compare token for opaque stored bytes.
547    ///
548    /// Custom [`RuntimeStore`] implementations use the same constructor for
549    /// both observations and successful CAS receipts; no decoded lifecycle
550    /// shape is allowed to stand in for the physical row version.
551    pub fn from_raw_record(bytes: &[u8]) -> Self {
552        Self(format!("sha256:{:x}", Sha256::digest(bytes)))
553    }
554
555    #[must_use]
556    pub fn as_str(&self) -> &str {
557        &self.0
558    }
559}
560
561/// Independently observed run-binding atoms.
562///
563/// A torn row may contain exactly one side of this pair. The store preserves
564/// that shape; the generated reconciler, not the decoder, decides whether it
565/// can be normalized.
566#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
567#[serde(rename_all = "snake_case")]
568pub enum MachineLifecyclePreRunPhase {
569    Idle,
570    Attached,
571    Retired,
572}
573
574#[derive(Debug, Clone, Default, PartialEq, Eq)]
575pub struct MachineLifecycleRunFacts {
576    current_run_id: Option<RunId>,
577    pre_run_phase: Option<MachineLifecyclePreRunPhase>,
578}
579
580impl MachineLifecycleRunFacts {
581    pub(crate) fn new(
582        current_run_id: Option<RunId>,
583        pre_run_phase: Option<MachineLifecyclePreRunPhase>,
584    ) -> Self {
585        Self {
586            current_run_id,
587            pre_run_phase,
588        }
589    }
590
591    #[must_use]
592    pub fn current_run_id(&self) -> Option<&RunId> {
593        self.current_run_id.as_ref()
594    }
595
596    #[must_use]
597    pub fn pre_run_phase(&self) -> Option<MachineLifecyclePreRunPhase> {
598        self.pre_run_phase
599    }
600}
601
602/// Decoded runtime-lifecycle observation.
603///
604/// The lifecycle phase, four binding atoms, and two run atoms remain
605/// independently optional. This type deliberately represents partial tuples
606/// such as `current_run_id = Some` with `pre_run_phase = None` instead of
607/// rejecting them as an impossible transition shape.
608#[derive(Debug, Clone, PartialEq, Eq)]
609pub struct DecodedMachineLifecycleObservation {
610    record_version: u16,
611    runtime_state: Option<RuntimeState>,
612    binding: MachineLifecycleBindingFacts,
613    run: MachineLifecycleRunFacts,
614    supervisor_authority: SupervisorAuthoritySnapshot,
615    unregister_progress: Option<MachineUnregisterProgressSnapshot>,
616}
617
618impl DecodedMachineLifecycleObservation {
619    #[must_use]
620    pub fn record_version(&self) -> u16 {
621        self.record_version
622    }
623
624    #[must_use]
625    pub fn runtime_state(&self) -> Option<RuntimeState> {
626        self.runtime_state
627    }
628
629    #[must_use]
630    pub fn binding(&self) -> &MachineLifecycleBindingFacts {
631        &self.binding
632    }
633
634    #[must_use]
635    pub fn run(&self) -> &MachineLifecycleRunFacts {
636        &self.run
637    }
638
639    #[must_use]
640    pub fn supervisor_authority(&self) -> &SupervisorAuthoritySnapshot {
641        &self.supervisor_authority
642    }
643
644    #[must_use]
645    pub fn unregister_progress(&self) -> Option<&MachineUnregisterProgressSnapshot> {
646        self.unregister_progress.as_ref()
647    }
648}
649
650/// Lossless classification of one physical machine-lifecycle row.
651///
652/// Transport failures remain [`RuntimeStoreError`] values. Every successfully
653/// read row is classified without collapsing unsupported or corrupt bytes into
654/// absence.
655#[derive(Debug, Clone, PartialEq, Eq)]
656pub enum MachineLifecycleObservation {
657    Missing,
658    Decoded {
659        record: DecodedMachineLifecycleObservation,
660        version: MachineLifecycleObservationVersion,
661    },
662    Unsupported {
663        record_version: u64,
664        evidence_digest: String,
665        version: MachineLifecycleObservationVersion,
666    },
667    Malformed {
668        record_version: Option<u64>,
669        evidence_digest: String,
670        version: MachineLifecycleObservationVersion,
671        detail: String,
672    },
673}
674
675impl MachineLifecycleObservation {
676    /// Losslessly classify one successfully read physical lifecycle row.
677    ///
678    /// This is the canonical adapter seam for custom stores. Transport
679    /// failure stays an outer [`RuntimeStoreError`]; every byte sequence read
680    /// successfully becomes Decoded, Unsupported, or Malformed here.
681    #[must_use]
682    pub fn from_raw_record(bytes: &[u8]) -> Self {
683        classify_machine_lifecycle_record(bytes)
684    }
685
686    #[must_use]
687    pub fn version(&self) -> Option<&MachineLifecycleObservationVersion> {
688        match self {
689            Self::Missing => None,
690            Self::Decoded { version, .. }
691            | Self::Unsupported { version, .. }
692            | Self::Malformed { version, .. } => Some(version),
693        }
694    }
695
696    #[must_use]
697    pub fn evidence_digest(&self) -> Option<&str> {
698        match self {
699            Self::Unsupported {
700                evidence_digest, ..
701            }
702            | Self::Malformed {
703                evidence_digest, ..
704            } => Some(evidence_digest),
705            Self::Missing | Self::Decoded { .. } => None,
706        }
707    }
708}
709
710/// Target-local precondition for lifecycle normalization.
711#[derive(Debug, Clone, PartialEq, Eq)]
712pub enum MachineLifecycleExpectedVersion {
713    Missing,
714    Version(MachineLifecycleObservationVersion),
715}
716
717impl MachineLifecycleObservation {
718    /// Exact target-local precondition represented by this observation.
719    ///
720    /// Missing is a first-class compare value. Every present row, including
721    /// unsupported and malformed bytes, is compared by its raw-content
722    /// version rather than by a decoded projection.
723    #[must_use]
724    pub fn expected_version(&self) -> MachineLifecycleExpectedVersion {
725        self.version()
726            .map_or(MachineLifecycleExpectedVersion::Missing, |version| {
727                MachineLifecycleExpectedVersion::Version(version.clone())
728            })
729    }
730}
731
732/// Result of executing one synchronous target write under an external fence.
733///
734/// The fence is deliberately runtime-generic. A caller may back it with a
735/// lease, process-incarnation lock, or another authority source without the
736/// runtime store depending on that owner's domain types.
737#[derive(Debug, Clone, PartialEq, Eq)]
738pub enum RuntimeStoreWriteFenceOutcome {
739    /// The fence was current and invoked the supplied operation exactly once.
740    Applied,
741    /// Durable authority was superseded. The operation was not invoked.
742    Conflict { reason: String },
743    /// Authority could not be checked temporarily. The operation was not
744    /// invoked and the caller should retry after re-observation.
745    Backoff { reason: String },
746}
747
748/// Synchronous authority guard for a RuntimeStore target write.
749///
750/// Implementations MUST retain their authority serialization guard for the
751/// full duration of `operation`, invoke it exactly once only when authority is
752/// current, and never invoke it for Conflict or Backoff. The operation is
753/// synchronous by design so built-in stores can call it inside their own lock
754/// or transaction immediately before the target write. Time-bounded authority
755/// must be evaluated using the authority store's own clock while that guard is
756/// held, never a caller-supplied observation timestamp. Once a successful
757/// operation returns, the fence must return Applied without a new fallible
758/// boundary. Implementations must not re-enter the same RuntimeStore from this
759/// callback.
760pub trait RuntimeStoreWriteFence: Send + Sync {
761    fn execute_if_current(
762        &self,
763        operation: Box<dyn FnOnce() -> Result<(), RuntimeStoreError> + '_>,
764    ) -> Result<RuntimeStoreWriteFenceOutcome, RuntimeStoreError>;
765}
766
767pub(crate) fn execute_runtime_store_write_fence(
768    write_fence: &dyn RuntimeStoreWriteFence,
769    operation: impl FnOnce() -> Result<(), RuntimeStoreError>,
770) -> Result<RuntimeStoreWriteFenceOutcome, RuntimeStoreError> {
771    let invoked = std::cell::Cell::new(false);
772    let operation_result = std::cell::RefCell::new(None);
773    let checked_operation = || {
774        invoked.set(true);
775        let result = operation();
776        *operation_result.borrow_mut() = Some(result.clone());
777        result
778    };
779    let outcome = write_fence.execute_if_current(Box::new(checked_operation))?;
780    if let Some(Err(error)) = operation_result.borrow_mut().take() {
781        return Err(error);
782    }
783    let shape_is_valid = matches!(
784        (&outcome, invoked.get()),
785        (RuntimeStoreWriteFenceOutcome::Applied, true)
786            | (
787                RuntimeStoreWriteFenceOutcome::Conflict { .. }
788                    | RuntimeStoreWriteFenceOutcome::Backoff { .. },
789                false,
790            )
791    );
792    if !shape_is_valid {
793        return Err(RuntimeStoreError::Internal(
794            "runtime write fence returned an outcome inconsistent with operation execution"
795                .to_string(),
796        ));
797    }
798    Ok(outcome)
799}
800
801/// Result of a target-local lifecycle CAS performed under an external fence.
802///
803/// Applied and AlreadyExact carry the exact decoded row used to construct the
804/// fresh process-local registration. Callers never receive or construct a
805/// MachineLifecycleCommit.
806#[derive(Debug, Clone, PartialEq, Eq)]
807pub enum FencedMachineLifecycleCasOutcome {
808    Applied {
809        record: DecodedMachineLifecycleObservation,
810        version: MachineLifecycleObservationVersion,
811    },
812    AlreadyExact {
813        record: DecodedMachineLifecycleObservation,
814        version: MachineLifecycleObservationVersion,
815    },
816    Conflict {
817        current: MachineLifecycleObservation,
818    },
819    FenceConflict {
820        reason: String,
821    },
822    FenceBackoff {
823        reason: String,
824    },
825}
826
827/// Result of a target-local lifecycle compare-and-swap.
828#[derive(Debug, Clone, PartialEq, Eq)]
829pub enum MachineLifecycleCasOutcome {
830    Applied {
831        version: MachineLifecycleObservationVersion,
832    },
833    Conflict {
834        current: MachineLifecycleObservation,
835    },
836}
837
838/// Durable read-back shape for machine-owned lifecycle state.
839#[derive(Debug, Clone, PartialEq, Eq)]
840pub struct MachineLifecycleSnapshot {
841    runtime_state: RuntimeState,
842    binding: MachineLifecycleBindingFacts,
843    run: MachineLifecycleRunFacts,
844    supervisor_authority: SupervisorAuthoritySnapshot,
845    unregister_progress: Option<MachineUnregisterProgressSnapshot>,
846}
847
848/// Durable generated unregister-saga progress needed to resume an interrupted
849/// Draining epoch without reconstructing missing producer outcomes in shell
850/// code.
851#[derive(Debug, Clone, PartialEq, Eq)]
852pub struct MachineUnregisterProgressSnapshot {
853    runtime_loop_drain_pending: bool,
854    comms_drain_exit_pending: bool,
855    completion_waiter_drain_pending: bool,
856    runtime_loop_forced_abort: bool,
857    comms_drain_forced_abort: bool,
858}
859
860impl MachineUnregisterProgressSnapshot {
861    pub(crate) fn new(
862        runtime_loop_drain_pending: bool,
863        comms_drain_exit_pending: bool,
864        completion_waiter_drain_pending: bool,
865        runtime_loop_forced_abort: bool,
866        comms_drain_forced_abort: bool,
867    ) -> Self {
868        Self {
869            runtime_loop_drain_pending,
870            comms_drain_exit_pending,
871            completion_waiter_drain_pending,
872            runtime_loop_forced_abort,
873            comms_drain_forced_abort,
874        }
875    }
876
877    pub(crate) fn runtime_loop_drain_pending(&self) -> bool {
878        self.runtime_loop_drain_pending
879    }
880
881    pub(crate) fn comms_drain_exit_pending(&self) -> bool {
882        self.comms_drain_exit_pending
883    }
884
885    pub(crate) fn completion_waiter_drain_pending(&self) -> bool {
886        self.completion_waiter_drain_pending
887    }
888
889    pub(crate) fn runtime_loop_forced_abort(&self) -> bool {
890        self.runtime_loop_forced_abort
891    }
892
893    pub(crate) fn comms_drain_forced_abort(&self) -> bool {
894        self.comms_drain_forced_abort
895    }
896}
897
898impl MachineLifecycleSnapshot {
899    pub(crate) fn new(
900        runtime_state: RuntimeState,
901        binding: MachineLifecycleBindingFacts,
902        supervisor_authority: SupervisorAuthoritySnapshot,
903    ) -> Self {
904        Self::new_with_unregister_progress(runtime_state, binding, supervisor_authority, None)
905    }
906
907    pub(crate) fn new_with_unregister_progress(
908        runtime_state: RuntimeState,
909        binding: MachineLifecycleBindingFacts,
910        supervisor_authority: SupervisorAuthoritySnapshot,
911        unregister_progress: Option<MachineUnregisterProgressSnapshot>,
912    ) -> Self {
913        Self::new_with_run_and_unregister_progress(
914            runtime_state,
915            binding,
916            MachineLifecycleRunFacts::default(),
917            supervisor_authority,
918            unregister_progress,
919        )
920    }
921
922    pub(crate) fn new_with_run_and_unregister_progress(
923        runtime_state: RuntimeState,
924        binding: MachineLifecycleBindingFacts,
925        run: MachineLifecycleRunFacts,
926        supervisor_authority: SupervisorAuthoritySnapshot,
927        unregister_progress: Option<MachineUnregisterProgressSnapshot>,
928    ) -> Self {
929        Self {
930            runtime_state,
931            binding,
932            run,
933            supervisor_authority,
934            unregister_progress,
935        }
936    }
937
938    /// Runtime state selected by the owning MeerkatMachine transition.
939    pub fn runtime_state(&self) -> RuntimeState {
940        self.runtime_state
941    }
942
943    /// Runtime binding facts selected by the owning MeerkatMachine transition.
944    pub fn binding(&self) -> &MachineLifecycleBindingFacts {
945        &self.binding
946    }
947
948    /// Independently persisted run-binding atoms.
949    pub fn run(&self) -> &MachineLifecycleRunFacts {
950        &self.run
951    }
952
953    pub fn supervisor_authority(&self) -> &SupervisorAuthoritySnapshot {
954        &self.supervisor_authority
955    }
956
957    pub fn unregister_progress(&self) -> Option<&MachineUnregisterProgressSnapshot> {
958        self.unregister_progress.as_ref()
959    }
960}
961
962#[allow(
963    clippy::option_option,
964    reason = "serde distinguishes missing from explicit null"
965)]
966fn deserialize_present_nullable<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
967where
968    D: serde::Deserializer<'de>,
969    T: serde::Deserialize<'de>,
970{
971    <Option<T> as serde::Deserialize>::deserialize(deserializer).map(Some)
972}
973
974#[allow(
975    clippy::option_option,
976    reason = "serde distinguishes missing from explicit null"
977)]
978fn require_present_nullable<T>(
979    value: Option<Option<T>>,
980    field: &str,
981) -> Result<Option<T>, RuntimeStoreError> {
982    value.ok_or_else(|| {
983        RuntimeStoreError::ReadFailed(format!(
984            "machine lifecycle field {field} is required (explicit null is allowed)"
985        ))
986    })
987}
988
989#[derive(serde::Serialize, serde::Deserialize)]
990#[serde(deny_unknown_fields)]
991struct MachineLifecycleBindingFactsStoreWire {
992    #[allow(
993        clippy::option_option,
994        reason = "serde distinguishes missing from explicit null"
995    )]
996    #[serde(default, deserialize_with = "deserialize_present_nullable")]
997    agent_runtime_id: Option<Option<String>>,
998    #[allow(
999        clippy::option_option,
1000        reason = "serde distinguishes missing from explicit null"
1001    )]
1002    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1003    fence_token: Option<Option<u64>>,
1004    #[allow(
1005        clippy::option_option,
1006        reason = "serde distinguishes missing from explicit null"
1007    )]
1008    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1009    runtime_generation: Option<Option<u64>>,
1010    #[allow(
1011        clippy::option_option,
1012        reason = "serde distinguishes missing from explicit null"
1013    )]
1014    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1015    runtime_epoch_id: Option<Option<String>>,
1016}
1017
1018#[derive(serde::Deserialize)]
1019#[serde(deny_unknown_fields)]
1020struct MachineLifecycleBindingFactsStoreWireV1 {
1021    agent_runtime_id: Option<String>,
1022    fence_token: Option<u64>,
1023    runtime_generation: Option<u64>,
1024    runtime_epoch_id: Option<String>,
1025}
1026
1027impl From<&MachineLifecycleBindingFacts> for MachineLifecycleBindingFactsStoreWire {
1028    fn from(binding: &MachineLifecycleBindingFacts) -> Self {
1029        Self {
1030            agent_runtime_id: Some(binding.agent_runtime_id().map(ToOwned::to_owned)),
1031            fence_token: Some(binding.fence_token()),
1032            runtime_generation: Some(binding.runtime_generation()),
1033            runtime_epoch_id: Some(binding.runtime_epoch_id().map(ToOwned::to_owned)),
1034        }
1035    }
1036}
1037
1038impl TryFrom<MachineLifecycleBindingFactsStoreWire> for MachineLifecycleBindingFacts {
1039    type Error = RuntimeStoreError;
1040
1041    fn try_from(binding: MachineLifecycleBindingFactsStoreWire) -> Result<Self, Self::Error> {
1042        Ok(Self::new(
1043            require_present_nullable(binding.agent_runtime_id, "binding.agent_runtime_id")?,
1044            require_present_nullable(binding.fence_token, "binding.fence_token")?,
1045            require_present_nullable(binding.runtime_generation, "binding.runtime_generation")?,
1046            require_present_nullable(binding.runtime_epoch_id, "binding.runtime_epoch_id")?,
1047        ))
1048    }
1049}
1050
1051impl From<MachineLifecycleBindingFactsStoreWireV1> for MachineLifecycleBindingFacts {
1052    fn from(binding: MachineLifecycleBindingFactsStoreWireV1) -> Self {
1053        Self::new(
1054            binding.agent_runtime_id,
1055            binding.fence_token,
1056            binding.runtime_generation,
1057            binding.runtime_epoch_id,
1058        )
1059    }
1060}
1061
1062#[derive(serde::Serialize)]
1063#[serde(deny_unknown_fields)]
1064struct MachineLifecycleSnapshotStoreWire {
1065    record_version: u16,
1066    runtime_state: RuntimeState,
1067    binding: MachineLifecycleBindingFactsStoreWire,
1068    current_run_id: Option<RunId>,
1069    pre_run_phase: Option<MachineLifecyclePreRunPhase>,
1070    supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1071    unregister_progress: Option<MachineUnregisterProgressSnapshotStoreWire>,
1072}
1073
1074#[derive(serde::Deserialize)]
1075#[serde(deny_unknown_fields)]
1076struct MachineLifecycleObservationStoreWireV4 {
1077    record_version: u16,
1078    #[allow(
1079        clippy::option_option,
1080        reason = "serde distinguishes a missing phase from an explicitly absent observed phase"
1081    )]
1082    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1083    runtime_state: Option<Option<RuntimeState>>,
1084    binding: MachineLifecycleBindingFactsStoreWire,
1085    #[allow(
1086        clippy::option_option,
1087        reason = "serde distinguishes a missing run id from an explicitly absent run id"
1088    )]
1089    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1090    current_run_id: Option<Option<RunId>>,
1091    #[allow(
1092        clippy::option_option,
1093        reason = "serde distinguishes a missing pre-run phase from an explicitly absent phase"
1094    )]
1095    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1096    pre_run_phase: Option<Option<MachineLifecyclePreRunPhase>>,
1097    supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1098    #[allow(
1099        clippy::option_option,
1100        reason = "serde distinguishes a missing v4 field from explicit null progress"
1101    )]
1102    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1103    unregister_progress: Option<Option<MachineUnregisterProgressSnapshotStoreWire>>,
1104}
1105
1106#[derive(serde::Deserialize)]
1107#[serde(deny_unknown_fields)]
1108struct MachineLifecycleSnapshotStoreWireV3 {
1109    record_version: u16,
1110    runtime_state: RuntimeState,
1111    binding: MachineLifecycleBindingFactsStoreWire,
1112    supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1113    #[allow(
1114        clippy::option_option,
1115        reason = "serde distinguishes a missing v3 field from explicit null progress"
1116    )]
1117    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1118    unregister_progress: Option<Option<MachineUnregisterProgressSnapshotStoreWire>>,
1119}
1120
1121#[derive(serde::Deserialize)]
1122#[serde(deny_unknown_fields)]
1123struct MachineLifecycleSnapshotStoreWireV2 {
1124    record_version: u16,
1125    runtime_state: RuntimeState,
1126    binding: MachineLifecycleBindingFactsStoreWire,
1127    supervisor_authority: SupervisorAuthoritySnapshotStoreWire,
1128}
1129
1130#[derive(serde::Serialize, serde::Deserialize)]
1131#[serde(deny_unknown_fields)]
1132struct MachineUnregisterProgressSnapshotStoreWire {
1133    runtime_loop_drain_pending: bool,
1134    comms_drain_exit_pending: bool,
1135    completion_waiter_drain_pending: bool,
1136    runtime_loop_forced_abort: bool,
1137    comms_drain_forced_abort: bool,
1138}
1139
1140impl From<&MachineUnregisterProgressSnapshot> for MachineUnregisterProgressSnapshotStoreWire {
1141    fn from(snapshot: &MachineUnregisterProgressSnapshot) -> Self {
1142        Self {
1143            runtime_loop_drain_pending: snapshot.runtime_loop_drain_pending(),
1144            comms_drain_exit_pending: snapshot.comms_drain_exit_pending(),
1145            completion_waiter_drain_pending: snapshot.completion_waiter_drain_pending(),
1146            runtime_loop_forced_abort: snapshot.runtime_loop_forced_abort(),
1147            comms_drain_forced_abort: snapshot.comms_drain_forced_abort(),
1148        }
1149    }
1150}
1151
1152impl From<MachineUnregisterProgressSnapshotStoreWire> for MachineUnregisterProgressSnapshot {
1153    fn from(snapshot: MachineUnregisterProgressSnapshotStoreWire) -> Self {
1154        Self::new(
1155            snapshot.runtime_loop_drain_pending,
1156            snapshot.comms_drain_exit_pending,
1157            snapshot.completion_waiter_drain_pending,
1158            snapshot.runtime_loop_forced_abort,
1159            snapshot.comms_drain_forced_abort,
1160        )
1161    }
1162}
1163
1164/// Exact pre-supervisor-authority lifecycle shape. Version 1 is decoded only
1165/// through this migration carrier so a missing authority on a current record
1166/// cannot be confused with legacy data.
1167#[derive(serde::Deserialize)]
1168#[serde(deny_unknown_fields)]
1169struct MachineLifecycleSnapshotStoreWireV1 {
1170    record_version: u16,
1171    runtime_state: RuntimeState,
1172    binding: MachineLifecycleBindingFactsStoreWireV1,
1173}
1174
1175#[derive(serde::Deserialize)]
1176struct MachineLifecycleSnapshotStoreVersionProbe {
1177    record_version: u16,
1178}
1179
1180#[derive(Default, serde::Serialize, serde::Deserialize)]
1181#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1182enum SupervisorAuthoritySnapshotStoreWire {
1183    #[default]
1184    UnboundNoReceipt,
1185    Bound {
1186        binding: SupervisorBindingReceiptStoreWire,
1187    },
1188    RevocationPending {
1189        pending: SupervisorRevocationPendingReceiptStoreWire,
1190    },
1191    RotationOperation {
1192        rotation: SupervisorRotationReceiptStoreWire,
1193    },
1194    RevokedReceipt {
1195        receipt: RevokedSupervisorReceiptStoreWire,
1196    },
1197    WithRotationHistory {
1198        current: Box<SupervisorAuthoritySnapshotStoreWire>,
1199        terminal_receipts: Vec<SupervisorRotationReceiptStoreWire>,
1200    },
1201}
1202
1203#[derive(serde::Serialize, serde::Deserialize)]
1204#[serde(deny_unknown_fields)]
1205struct SupervisorBindingReceiptStoreWire {
1206    name: String,
1207    peer_id: String,
1208    address: String,
1209    signing_public_key: String,
1210    epoch: u64,
1211}
1212
1213impl From<&SupervisorBindingReceipt> for SupervisorBindingReceiptStoreWire {
1214    fn from(receipt: &SupervisorBindingReceipt) -> Self {
1215        Self {
1216            name: receipt.name().to_owned(),
1217            peer_id: receipt.peer_id().to_owned(),
1218            address: receipt.address().to_owned(),
1219            signing_public_key: receipt.signing_public_key().to_owned(),
1220            epoch: receipt.epoch(),
1221        }
1222    }
1223}
1224
1225impl From<SupervisorBindingReceiptStoreWire> for SupervisorBindingReceipt {
1226    fn from(receipt: SupervisorBindingReceiptStoreWire) -> Self {
1227        Self::new(
1228            receipt.name,
1229            receipt.peer_id,
1230            receipt.address,
1231            receipt.signing_public_key,
1232            receipt.epoch,
1233        )
1234    }
1235}
1236
1237#[derive(serde::Serialize, serde::Deserialize)]
1238#[serde(deny_unknown_fields)]
1239struct RevokedSupervisorReceiptStoreWire {
1240    peer_id: String,
1241    signing_public_key: String,
1242    epoch: u64,
1243}
1244
1245#[derive(serde::Serialize, serde::Deserialize)]
1246#[serde(deny_unknown_fields)]
1247struct SupervisorRevocationPendingReceiptStoreWire {
1248    name: String,
1249    peer_id: String,
1250    address: String,
1251    signing_public_key: String,
1252    epoch: u64,
1253}
1254
1255#[derive(serde::Serialize, serde::Deserialize)]
1256#[serde(deny_unknown_fields)]
1257struct SupervisorRotationReceiptStoreWire {
1258    operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
1259    phase: SupervisorRotationPersistencePhase,
1260    #[allow(
1261        clippy::option_option,
1262        reason = "serde distinguishes missing from explicit null"
1263    )]
1264    #[serde(default, deserialize_with = "deserialize_present_nullable")]
1265    rejection: Option<Option<SupervisorRotationRejection>>,
1266    previous: SupervisorBindingReceiptStoreWire,
1267    next: SupervisorBindingReceiptStoreWire,
1268}
1269
1270impl From<&SupervisorRotationReceipt> for SupervisorRotationReceiptStoreWire {
1271    fn from(receipt: &SupervisorRotationReceipt) -> Self {
1272        Self {
1273            operation_id: receipt.operation_id(),
1274            phase: receipt.phase(),
1275            rejection: Some(receipt.rejection()),
1276            previous: receipt.previous().into(),
1277            next: receipt.next().into(),
1278        }
1279    }
1280}
1281
1282impl TryFrom<SupervisorRotationReceiptStoreWire> for SupervisorRotationReceipt {
1283    type Error = RuntimeStoreError;
1284
1285    fn try_from(receipt: SupervisorRotationReceiptStoreWire) -> Result<Self, Self::Error> {
1286        Ok(Self::new(
1287            receipt.operation_id,
1288            receipt.phase,
1289            require_present_nullable(receipt.rejection, "supervisor_authority.rotation.rejection")?,
1290            receipt.previous.into(),
1291            receipt.next.into(),
1292        ))
1293    }
1294}
1295
1296impl From<&SupervisorRevocationPendingReceipt> for SupervisorRevocationPendingReceiptStoreWire {
1297    fn from(receipt: &SupervisorRevocationPendingReceipt) -> Self {
1298        Self {
1299            name: receipt.name().to_owned(),
1300            peer_id: receipt.peer_id().to_owned(),
1301            address: receipt.address().to_owned(),
1302            signing_public_key: receipt.signing_public_key().to_owned(),
1303            epoch: receipt.epoch(),
1304        }
1305    }
1306}
1307
1308impl From<SupervisorRevocationPendingReceiptStoreWire> for SupervisorRevocationPendingReceipt {
1309    fn from(receipt: SupervisorRevocationPendingReceiptStoreWire) -> Self {
1310        Self::new(
1311            receipt.name,
1312            receipt.peer_id,
1313            receipt.address,
1314            receipt.signing_public_key,
1315            receipt.epoch,
1316        )
1317    }
1318}
1319
1320impl From<&RevokedSupervisorReceipt> for RevokedSupervisorReceiptStoreWire {
1321    fn from(receipt: &RevokedSupervisorReceipt) -> Self {
1322        Self {
1323            peer_id: receipt.peer_id().to_owned(),
1324            signing_public_key: receipt.signing_public_key().to_owned(),
1325            epoch: receipt.epoch(),
1326        }
1327    }
1328}
1329
1330impl From<RevokedSupervisorReceiptStoreWire> for RevokedSupervisorReceipt {
1331    fn from(receipt: RevokedSupervisorReceiptStoreWire) -> Self {
1332        Self::new(receipt.peer_id, receipt.signing_public_key, receipt.epoch)
1333    }
1334}
1335
1336impl From<&SupervisorAuthoritySnapshot> for SupervisorAuthoritySnapshotStoreWire {
1337    fn from(snapshot: &SupervisorAuthoritySnapshot) -> Self {
1338        match snapshot {
1339            SupervisorAuthoritySnapshot::UnboundNoReceipt => Self::UnboundNoReceipt,
1340            SupervisorAuthoritySnapshot::Bound(binding) => Self::Bound {
1341                binding: binding.into(),
1342            },
1343            SupervisorAuthoritySnapshot::RevocationPending(pending) => Self::RevocationPending {
1344                pending: pending.into(),
1345            },
1346            SupervisorAuthoritySnapshot::RotationOperation(rotation) => Self::RotationOperation {
1347                rotation: rotation.into(),
1348            },
1349            SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => Self::RevokedReceipt {
1350                receipt: receipt.into(),
1351            },
1352            SupervisorAuthoritySnapshot::WithRotationHistory {
1353                current,
1354                terminal_receipts,
1355            } => Self::WithRotationHistory {
1356                current: Box::new(current.as_ref().into()),
1357                terminal_receipts: terminal_receipts.values().map(Into::into).collect(),
1358            },
1359        }
1360    }
1361}
1362
1363fn supervisor_authority_read_error(
1364    context: &str,
1365    detail: impl std::fmt::Display,
1366) -> RuntimeStoreError {
1367    RuntimeStoreError::ReadFailed(format!("{context}: {detail}"))
1368}
1369
1370fn validate_supervisor_descriptor(
1371    name: &str,
1372    peer_id: &str,
1373    address: &str,
1374    signing_public_key: &str,
1375    context: &str,
1376) -> Result<(), RuntimeStoreError> {
1377    let pubkey = crate::comms_drain::decode_supervisor_signing_public_key(signing_public_key)
1378        .map_err(|error| supervisor_authority_read_error(context, error))?;
1379    let spec = meerkat_contracts::wire::supervisor_bridge::BridgePeerSpec {
1380        name: name.to_owned(),
1381        peer_id: peer_id.to_owned(),
1382        address: address.to_owned(),
1383        pubkey,
1384    };
1385    meerkat_core::comms::TrustedPeerDescriptor::try_from(&spec)
1386        .map(|_| ())
1387        .map_err(|error| supervisor_authority_read_error(context, error))
1388}
1389
1390fn validate_supervisor_binding_receipt(
1391    receipt: &SupervisorBindingReceipt,
1392    context: &str,
1393) -> Result<(), RuntimeStoreError> {
1394    validate_supervisor_descriptor(
1395        receipt.name(),
1396        receipt.peer_id(),
1397        receipt.address(),
1398        receipt.signing_public_key(),
1399        context,
1400    )
1401}
1402
1403fn validate_revoked_supervisor_receipt(
1404    receipt: &RevokedSupervisorReceipt,
1405    context: &str,
1406) -> Result<(), RuntimeStoreError> {
1407    let pubkey =
1408        crate::comms_drain::decode_supervisor_signing_public_key(receipt.signing_public_key())
1409            .map_err(|error| supervisor_authority_read_error(context, error))?;
1410    if pubkey.iter().all(|byte| *byte == 0) {
1411        return Err(supervisor_authority_read_error(
1412            context,
1413            "supervisor signing public key must be non-zero",
1414        ));
1415    }
1416    let peer_id = meerkat_core::comms::PeerId::parse(receipt.peer_id())
1417        .map_err(|error| supervisor_authority_read_error(context, error))?;
1418    let derived = meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey);
1419    if peer_id != derived {
1420        return Err(supervisor_authority_read_error(
1421            context,
1422            format!("peer id {peer_id} does not match signing-key-derived id {derived}"),
1423        ));
1424    }
1425    Ok(())
1426}
1427
1428fn validate_supervisor_rotation_receipt(
1429    receipt: &SupervisorRotationReceipt,
1430    terminal_history: bool,
1431) -> Result<(), RuntimeStoreError> {
1432    let operation_id = receipt.operation_id();
1433    if operation_id.as_uuid().is_nil() {
1434        return Err(supervisor_authority_read_error(
1435            "supervisor rotation operation",
1436            "operation id must not be the nil UUID",
1437        ));
1438    }
1439    validate_supervisor_binding_receipt(
1440        receipt.previous(),
1441        &format!("supervisor rotation {operation_id} previous authority is invalid"),
1442    )?;
1443
1444    let rejection_matches = matches!(
1445        (receipt.phase(), receipt.rejection()),
1446        (
1447            SupervisorRotationPersistencePhase::PreviousRevokePending
1448                | SupervisorRotationPersistencePhase::NextPublishPending
1449                | SupervisorRotationPersistencePhase::Completed,
1450            None
1451        ) | (SupervisorRotationPersistencePhase::Rejected, Some(_))
1452    );
1453    if !rejection_matches {
1454        return Err(supervisor_authority_read_error(
1455            "supervisor rotation operation",
1456            format!("{operation_id} has inconsistent rejection state"),
1457        ));
1458    }
1459    if terminal_history
1460        && !matches!(
1461            receipt.phase(),
1462            SupervisorRotationPersistencePhase::Completed
1463                | SupervisorRotationPersistencePhase::Rejected
1464        )
1465    {
1466        return Err(supervisor_authority_read_error(
1467            "supervisor rotation history",
1468            format!("{operation_id} is not terminal"),
1469        ));
1470    }
1471
1472    match receipt.phase() {
1473        SupervisorRotationPersistencePhase::PreviousRevokePending
1474        | SupervisorRotationPersistencePhase::NextPublishPending => {
1475            validate_supervisor_binding_receipt(
1476                receipt.next(),
1477                &format!("supervisor rotation {operation_id} target is invalid"),
1478            )?;
1479            if receipt.next().epoch() <= receipt.previous().epoch() {
1480                return Err(supervisor_authority_read_error(
1481                    "supervisor rotation operation",
1482                    format!(
1483                        "{operation_id} target epoch {} does not advance previous epoch {}",
1484                        receipt.next().epoch(),
1485                        receipt.previous().epoch()
1486                    ),
1487                ));
1488            }
1489        }
1490        SupervisorRotationPersistencePhase::Completed => {
1491            validate_supervisor_binding_receipt(
1492                receipt.next(),
1493                &format!("supervisor rotation {operation_id} target is invalid"),
1494            )?;
1495            // A legacy member may already have the exact target installed
1496            // before the operation protocol assigns an id. Its adoption
1497            // receipt is Completed with an exact previous == next witness.
1498            let exact_current_adoption = receipt.previous() == receipt.next();
1499            if !exact_current_adoption && receipt.next().epoch() <= receipt.previous().epoch() {
1500                return Err(supervisor_authority_read_error(
1501                    "supervisor rotation operation",
1502                    format!(
1503                        "{operation_id} completed target epoch {} does not advance previous epoch {}",
1504                        receipt.next().epoch(),
1505                        receipt.previous().epoch()
1506                    ),
1507                ));
1508            }
1509        }
1510        SupervisorRotationPersistencePhase::Rejected => {
1511            let Some(rejection) = receipt.rejection() else {
1512                return Err(supervisor_authority_read_error(
1513                    "supervisor rotation operation",
1514                    format!("{operation_id} rejected without a rejection class"),
1515                ));
1516            };
1517            match rejection {
1518                SupervisorRotationRejection::InvalidTarget
1519                | SupervisorRotationRejection::UnsupportedProtocolVersion => {
1520                    // These two rejection classes retain the undecodable target
1521                    // fields as raw evidence. They are deliberately exempt from
1522                    // target descriptor validation and epoch comparison.
1523                }
1524                SupervisorRotationRejection::TargetEpochNotAdvanced => {
1525                    validate_supervisor_binding_receipt(
1526                        receipt.next(),
1527                        &format!("supervisor rotation {operation_id} rejected target is invalid"),
1528                    )?;
1529                    if receipt.next().epoch() > receipt.previous().epoch() {
1530                        return Err(supervisor_authority_read_error(
1531                            "supervisor rotation operation",
1532                            format!(
1533                                "{operation_id} rejected as non-advancing but target epoch {} advances previous epoch {}",
1534                                receipt.next().epoch(),
1535                                receipt.previous().epoch()
1536                            ),
1537                        ));
1538                    }
1539                }
1540                SupervisorRotationRejection::OperationConflict
1541                | SupervisorRotationRejection::NotBound
1542                | SupervisorRotationRejection::SenderMismatch => {
1543                    return Err(supervisor_authority_read_error(
1544                        "supervisor rotation operation",
1545                        format!(
1546                            "{operation_id} transient rejection {rejection:?} must not be persisted as a durable receipt"
1547                        ),
1548                    ));
1549                }
1550            }
1551        }
1552    }
1553    Ok(())
1554}
1555
1556type SupervisorEpochKeyIndex = std::collections::BTreeMap<u64, [u8; 32]>;
1557
1558fn record_supervisor_epoch_key(
1559    epochs: &mut SupervisorEpochKeyIndex,
1560    epoch: u64,
1561    signing_public_key: &str,
1562    context: &str,
1563) -> Result<(), RuntimeStoreError> {
1564    let key = crate::comms_drain::decode_supervisor_signing_public_key(signing_public_key)
1565        .map_err(|error| supervisor_authority_read_error(context, error))?;
1566    if let Some(existing) = epochs.get(&epoch) {
1567        if existing != &key {
1568            return Err(supervisor_authority_read_error(
1569                context,
1570                format!("epoch {epoch} is bound to conflicting supervisor signing keys"),
1571            ));
1572        }
1573    } else {
1574        epochs.insert(epoch, key);
1575    }
1576    Ok(())
1577}
1578
1579fn record_supervisor_binding_epoch(
1580    epochs: &mut SupervisorEpochKeyIndex,
1581    receipt: &SupervisorBindingReceipt,
1582    context: &str,
1583) -> Result<(), RuntimeStoreError> {
1584    record_supervisor_epoch_key(
1585        epochs,
1586        receipt.epoch(),
1587        receipt.signing_public_key(),
1588        context,
1589    )
1590}
1591
1592fn record_rotation_authoritative_epochs(
1593    epochs: &mut SupervisorEpochKeyIndex,
1594    receipt: &SupervisorRotationReceipt,
1595    context: &str,
1596) -> Result<(), RuntimeStoreError> {
1597    record_supervisor_binding_epoch(epochs, receipt.previous(), context)?;
1598    if matches!(
1599        receipt.phase(),
1600        SupervisorRotationPersistencePhase::PreviousRevokePending
1601            | SupervisorRotationPersistencePhase::NextPublishPending
1602            | SupervisorRotationPersistencePhase::Completed
1603    ) {
1604        record_supervisor_binding_epoch(epochs, receipt.next(), context)?;
1605    }
1606    Ok(())
1607}
1608
1609fn record_current_authoritative_epochs(
1610    epochs: &mut SupervisorEpochKeyIndex,
1611    current: &SupervisorAuthoritySnapshot,
1612) -> Result<(), RuntimeStoreError> {
1613    match current {
1614        SupervisorAuthoritySnapshot::UnboundNoReceipt => Ok(()),
1615        SupervisorAuthoritySnapshot::Bound(binding) => {
1616            record_supervisor_binding_epoch(epochs, binding, "current supervisor authority")
1617        }
1618        SupervisorAuthoritySnapshot::RevocationPending(pending) => record_supervisor_epoch_key(
1619            epochs,
1620            pending.epoch(),
1621            pending.signing_public_key(),
1622            "current pending supervisor revocation authority",
1623        ),
1624        SupervisorAuthoritySnapshot::RotationOperation(rotation) => {
1625            record_rotation_authoritative_epochs(
1626                epochs,
1627                rotation,
1628                "current supervisor rotation authority",
1629            )
1630        }
1631        SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => record_supervisor_epoch_key(
1632            epochs,
1633            receipt.epoch(),
1634            receipt.signing_public_key(),
1635            "current revoked supervisor authority",
1636        ),
1637        SupervisorAuthoritySnapshot::WithRotationHistory { .. } => {
1638            Err(RuntimeStoreError::ReadFailed(
1639                "nested supervisor rotation history is not canonical".to_string(),
1640            ))
1641        }
1642    }
1643}
1644
1645fn current_supervisor_epoch(current: &SupervisorAuthoritySnapshot) -> Option<u64> {
1646    match current {
1647        SupervisorAuthoritySnapshot::UnboundNoReceipt => None,
1648        SupervisorAuthoritySnapshot::Bound(binding) => Some(binding.epoch()),
1649        SupervisorAuthoritySnapshot::RevocationPending(pending) => Some(pending.epoch()),
1650        SupervisorAuthoritySnapshot::RotationOperation(rotation) => Some(match rotation.phase() {
1651            SupervisorRotationPersistencePhase::PreviousRevokePending
1652            | SupervisorRotationPersistencePhase::Rejected => rotation.previous().epoch(),
1653            SupervisorRotationPersistencePhase::NextPublishPending
1654            | SupervisorRotationPersistencePhase::Completed => rotation.next().epoch(),
1655        }),
1656        SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => Some(receipt.epoch()),
1657        SupervisorAuthoritySnapshot::WithRotationHistory { .. } => None,
1658    }
1659}
1660
1661fn terminal_rotation_authority_epoch(receipt: &SupervisorRotationReceipt) -> u64 {
1662    match receipt.phase() {
1663        SupervisorRotationPersistencePhase::Completed => receipt.next().epoch(),
1664        SupervisorRotationPersistencePhase::Rejected => receipt.previous().epoch(),
1665        SupervisorRotationPersistencePhase::PreviousRevokePending
1666        | SupervisorRotationPersistencePhase::NextPublishPending => receipt.previous().epoch(),
1667    }
1668}
1669
1670fn validate_supervisor_rotation_history_coherence(
1671    current: &SupervisorAuthoritySnapshot,
1672    terminal_receipts: &std::collections::BTreeMap<
1673        meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
1674        SupervisorRotationReceipt,
1675    >,
1676) -> Result<(), RuntimeStoreError> {
1677    let Some(current_epoch) = current_supervisor_epoch(current) else {
1678        return Err(RuntimeStoreError::ReadFailed(
1679            "supervisor rotation history requires a current authority epoch".to_string(),
1680        ));
1681    };
1682
1683    let mut epochs = SupervisorEpochKeyIndex::new();
1684    record_current_authoritative_epochs(&mut epochs, current)?;
1685    let mut history_high_water = 0;
1686    for receipt in terminal_receipts.values() {
1687        record_rotation_authoritative_epochs(
1688            &mut epochs,
1689            receipt,
1690            "supervisor rotation history authority",
1691        )?;
1692        history_high_water = history_high_water.max(terminal_rotation_authority_epoch(receipt));
1693    }
1694    if current_epoch < history_high_water {
1695        return Err(RuntimeStoreError::ReadFailed(format!(
1696            "current supervisor epoch {current_epoch} is below terminal rotation history high-water {history_high_water}"
1697        )));
1698    }
1699    Ok(())
1700}
1701
1702fn validate_supervisor_authority_snapshot(
1703    snapshot: &SupervisorAuthoritySnapshot,
1704) -> Result<(), RuntimeStoreError> {
1705    match snapshot {
1706        SupervisorAuthoritySnapshot::UnboundNoReceipt => Ok(()),
1707        SupervisorAuthoritySnapshot::Bound(binding) => {
1708            validate_supervisor_binding_receipt(binding, "bound supervisor is invalid")
1709        }
1710        SupervisorAuthoritySnapshot::RevocationPending(pending) => validate_supervisor_descriptor(
1711            pending.name(),
1712            pending.peer_id(),
1713            pending.address(),
1714            pending.signing_public_key(),
1715            "pending supervisor revocation authority is invalid",
1716        ),
1717        SupervisorAuthoritySnapshot::RotationOperation(rotation) => {
1718            validate_supervisor_rotation_receipt(rotation, false)
1719        }
1720        SupervisorAuthoritySnapshot::RevokedReceipt(receipt) => {
1721            validate_revoked_supervisor_receipt(receipt, "revoked supervisor receipt is invalid")
1722        }
1723        SupervisorAuthoritySnapshot::WithRotationHistory {
1724            current,
1725            terminal_receipts,
1726        } => {
1727            if matches!(
1728                current.as_ref(),
1729                SupervisorAuthoritySnapshot::WithRotationHistory { .. }
1730            ) {
1731                return Err(RuntimeStoreError::ReadFailed(
1732                    "nested supervisor rotation history is not canonical".to_string(),
1733                ));
1734            }
1735            if terminal_receipts.is_empty() {
1736                return Err(RuntimeStoreError::ReadFailed(
1737                    "empty supervisor rotation history wrapper is not canonical".to_string(),
1738                ));
1739            }
1740            validate_supervisor_authority_snapshot(current)?;
1741            for (operation_id, receipt) in terminal_receipts {
1742                if operation_id != &receipt.operation_id() {
1743                    return Err(RuntimeStoreError::ReadFailed(format!(
1744                        "supervisor rotation history key {operation_id} does not match receipt id {}",
1745                        receipt.operation_id()
1746                    )));
1747                }
1748                validate_supervisor_rotation_receipt(receipt, true)?;
1749            }
1750            if let SupervisorAuthoritySnapshot::RotationOperation(active) = current.as_ref()
1751                && terminal_receipts.contains_key(&active.operation_id())
1752            {
1753                return Err(RuntimeStoreError::ReadFailed(
1754                    "active supervisor rotation is duplicated in terminal history".to_string(),
1755                ));
1756            }
1757            validate_supervisor_rotation_history_coherence(current, terminal_receipts)
1758        }
1759    }
1760}
1761
1762impl TryFrom<SupervisorAuthoritySnapshotStoreWire> for SupervisorAuthoritySnapshot {
1763    type Error = RuntimeStoreError;
1764
1765    fn try_from(snapshot: SupervisorAuthoritySnapshotStoreWire) -> Result<Self, Self::Error> {
1766        match snapshot {
1767            SupervisorAuthoritySnapshotStoreWire::UnboundNoReceipt => Ok(Self::UnboundNoReceipt),
1768            SupervisorAuthoritySnapshotStoreWire::Bound { binding } => {
1769                let binding = binding.into();
1770                validate_supervisor_binding_receipt(&binding, "bound supervisor is invalid")?;
1771                Ok(Self::Bound(binding))
1772            }
1773            SupervisorAuthoritySnapshotStoreWire::RevocationPending { pending } => {
1774                let pending: SupervisorRevocationPendingReceipt = pending.into();
1775                validate_supervisor_descriptor(
1776                    pending.name(),
1777                    pending.peer_id(),
1778                    pending.address(),
1779                    pending.signing_public_key(),
1780                    "pending supervisor revocation authority is invalid",
1781                )?;
1782                Ok(Self::RevocationPending(pending))
1783            }
1784            SupervisorAuthoritySnapshotStoreWire::RotationOperation { rotation } => {
1785                let receipt: SupervisorRotationReceipt = rotation.try_into()?;
1786                validate_supervisor_rotation_receipt(&receipt, false)?;
1787                Ok(Self::RotationOperation(receipt))
1788            }
1789            SupervisorAuthoritySnapshotStoreWire::RevokedReceipt { receipt } => {
1790                let receipt = receipt.into();
1791                validate_revoked_supervisor_receipt(
1792                    &receipt,
1793                    "revoked supervisor receipt is invalid",
1794                )?;
1795                Ok(Self::RevokedReceipt(receipt))
1796            }
1797            SupervisorAuthoritySnapshotStoreWire::WithRotationHistory {
1798                current,
1799                terminal_receipts,
1800            } => {
1801                if terminal_receipts.is_empty() {
1802                    return Err(RuntimeStoreError::ReadFailed(
1803                        "empty supervisor rotation history wrapper is not canonical".to_string(),
1804                    ));
1805                }
1806                let current = Self::try_from(*current)?;
1807                if matches!(current, Self::WithRotationHistory { .. }) {
1808                    return Err(RuntimeStoreError::ReadFailed(
1809                        "nested supervisor rotation history is not canonical".to_string(),
1810                    ));
1811                }
1812                let mut receipts = std::collections::BTreeMap::new();
1813                for wire in terminal_receipts {
1814                    let receipt: SupervisorRotationReceipt = wire.try_into()?;
1815                    validate_supervisor_rotation_receipt(&receipt, true)?;
1816                    if receipts.insert(receipt.operation_id(), receipt).is_some() {
1817                        return Err(RuntimeStoreError::ReadFailed(
1818                            "supervisor rotation history contains a duplicate operation id"
1819                                .to_string(),
1820                        ));
1821                    }
1822                }
1823                if let Self::RotationOperation(active) = &current
1824                    && receipts.contains_key(&active.operation_id())
1825                {
1826                    return Err(RuntimeStoreError::ReadFailed(
1827                        "active supervisor rotation is duplicated in terminal history".to_string(),
1828                    ));
1829                }
1830                let snapshot = Self::WithRotationHistory {
1831                    current: Box::new(current),
1832                    terminal_receipts: receipts,
1833                };
1834                validate_supervisor_authority_snapshot(&snapshot)?;
1835                Ok(snapshot)
1836            }
1837        }
1838    }
1839}
1840
1841impl From<&MachineLifecycleSnapshot> for MachineLifecycleSnapshotStoreWire {
1842    fn from(snapshot: &MachineLifecycleSnapshot) -> Self {
1843        Self {
1844            record_version: MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
1845            runtime_state: snapshot.runtime_state(),
1846            binding: snapshot.binding().into(),
1847            current_run_id: snapshot.run().current_run_id().cloned(),
1848            pre_run_phase: snapshot.run().pre_run_phase(),
1849            supervisor_authority: snapshot.supervisor_authority().into(),
1850            unregister_progress: snapshot.unregister_progress().map(Into::into),
1851        }
1852    }
1853}
1854
1855fn validate_unregister_progress_snapshot(
1856    progress: Option<&MachineUnregisterProgressSnapshot>,
1857) -> Result<(), RuntimeStoreError> {
1858    if let Some(progress) = progress {
1859        if progress.runtime_loop_drain_pending() && progress.runtime_loop_forced_abort() {
1860            return Err(RuntimeStoreError::ReadFailed(
1861                "unregister runtime-loop forced disposition cannot precede obligation closure"
1862                    .into(),
1863            ));
1864        }
1865        if progress.comms_drain_exit_pending() && progress.comms_drain_forced_abort() {
1866            return Err(RuntimeStoreError::ReadFailed(
1867                "unregister comms-drain forced disposition cannot precede obligation closure"
1868                    .into(),
1869            ));
1870        }
1871    }
1872    Ok(())
1873}
1874
1875impl TryFrom<MachineLifecycleSnapshotStoreWireV3> for MachineLifecycleSnapshot {
1876    type Error = RuntimeStoreError;
1877
1878    fn try_from(record: MachineLifecycleSnapshotStoreWireV3) -> Result<Self, Self::Error> {
1879        if record.record_version != UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1880            return Err(RuntimeStoreError::ReadFailed(format!(
1881                "unsupported machine lifecycle store record version {}",
1882                record.record_version
1883            )));
1884        }
1885        let unregister_progress =
1886            require_present_nullable(record.unregister_progress, "unregister_progress")?
1887                .map(Into::into);
1888        validate_unregister_progress_snapshot(unregister_progress.as_ref())?;
1889        Ok(Self::new_with_unregister_progress(
1890            record.runtime_state,
1891            record.binding.try_into()?,
1892            record.supervisor_authority.try_into()?,
1893            unregister_progress,
1894        ))
1895    }
1896}
1897
1898fn decode_machine_lifecycle_observation_v4(
1899    bytes: &[u8],
1900) -> Result<DecodedMachineLifecycleObservation, RuntimeStoreError> {
1901    let record = serde_json::from_slice::<MachineLifecycleObservationStoreWireV4>(bytes)
1902        .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1903    if record.record_version != MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1904        return Err(RuntimeStoreError::ReadFailed(format!(
1905            "unsupported machine lifecycle store record version {}",
1906            record.record_version
1907        )));
1908    }
1909    let runtime_state = require_present_nullable(record.runtime_state, "runtime_state")?;
1910    let current_run_id = require_present_nullable(record.current_run_id, "current_run_id")?;
1911    let pre_run_phase = require_present_nullable(record.pre_run_phase, "pre_run_phase")?;
1912    let unregister_progress =
1913        require_present_nullable(record.unregister_progress, "unregister_progress")?
1914            .map(Into::into);
1915    validate_unregister_progress_snapshot(unregister_progress.as_ref())?;
1916    Ok(DecodedMachineLifecycleObservation {
1917        record_version: record.record_version,
1918        runtime_state,
1919        binding: record.binding.try_into()?,
1920        run: MachineLifecycleRunFacts::new(current_run_id, pre_run_phase),
1921        supervisor_authority: record.supervisor_authority.try_into()?,
1922        unregister_progress,
1923    })
1924}
1925
1926fn decoded_machine_lifecycle_from_snapshot(
1927    record_version: u16,
1928    snapshot: MachineLifecycleSnapshot,
1929) -> DecodedMachineLifecycleObservation {
1930    DecodedMachineLifecycleObservation {
1931        record_version,
1932        runtime_state: Some(snapshot.runtime_state),
1933        binding: snapshot.binding,
1934        run: snapshot.run,
1935        supervisor_authority: snapshot.supervisor_authority,
1936        unregister_progress: snapshot.unregister_progress,
1937    }
1938}
1939
1940fn decode_machine_lifecycle_store_record(
1941    bytes: &[u8],
1942) -> Result<MachineLifecycleSnapshot, RuntimeStoreError> {
1943    let version = serde_json::from_slice::<MachineLifecycleSnapshotStoreVersionProbe>(bytes)
1944        .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1945    match version.record_version {
1946        LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1947            let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV1>(bytes)
1948                .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1949            if record.record_version != LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1950                return Err(RuntimeStoreError::ReadFailed(format!(
1951                    "unsupported machine lifecycle store record version {}",
1952                    record.record_version
1953                )));
1954            }
1955            Ok(MachineLifecycleSnapshot::new(
1956                record.runtime_state,
1957                record.binding.into(),
1958                SupervisorAuthoritySnapshot::UnboundNoReceipt,
1959            ))
1960        }
1961        SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1962            let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV2>(bytes)
1963                .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1964            if record.record_version != SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION {
1965                return Err(RuntimeStoreError::ReadFailed(format!(
1966                    "unsupported machine lifecycle store record version {}",
1967                    record.record_version
1968                )));
1969            }
1970            Ok(MachineLifecycleSnapshot::new(
1971                record.runtime_state,
1972                record.binding.try_into()?,
1973                record.supervisor_authority.try_into()?,
1974            ))
1975        }
1976        UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1977            let record = serde_json::from_slice::<MachineLifecycleSnapshotStoreWireV3>(bytes)
1978                .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))?;
1979            MachineLifecycleSnapshot::try_from(record)
1980        }
1981        MACHINE_LIFECYCLE_STORE_RECORD_VERSION => {
1982            let record = decode_machine_lifecycle_observation_v4(bytes)?;
1983            let runtime_state = record.runtime_state.ok_or_else(|| {
1984                RuntimeStoreError::ReadFailed(
1985                    "machine lifecycle runtime_state cannot be null for strict recovery".into(),
1986                )
1987            })?;
1988            Ok(
1989                MachineLifecycleSnapshot::new_with_run_and_unregister_progress(
1990                    runtime_state,
1991                    record.binding,
1992                    record.run,
1993                    record.supervisor_authority,
1994                    record.unregister_progress,
1995                ),
1996            )
1997        }
1998        unsupported => Err(RuntimeStoreError::ReadFailed(format!(
1999            "unsupported machine lifecycle store record version {unsupported}"
2000        ))),
2001    }
2002}
2003
2004#[derive(serde::Deserialize)]
2005struct MachineLifecycleRawVersionProbe {
2006    record_version: u64,
2007}
2008
2009fn machine_lifecycle_record_version(bytes: &[u8]) -> Result<u64, String> {
2010    serde_json::from_slice::<MachineLifecycleRawVersionProbe>(bytes)
2011        .map(|probe| probe.record_version)
2012        .map_err(|error| {
2013            format!("machine lifecycle record_version is not uniquely readable: {error}")
2014        })
2015}
2016
2017fn classify_machine_lifecycle_record(bytes: &[u8]) -> MachineLifecycleObservation {
2018    let version = MachineLifecycleObservationVersion::from_raw_record(bytes);
2019    let evidence_digest = version.as_str().to_owned();
2020    let record_version = match machine_lifecycle_record_version(bytes) {
2021        Ok(record_version) => record_version,
2022        Err(detail) => {
2023            return MachineLifecycleObservation::Malformed {
2024                record_version: None,
2025                evidence_digest,
2026                version,
2027                detail,
2028            };
2029        }
2030    };
2031
2032    let supported = [
2033        u64::from(LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2034        u64::from(SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2035        u64::from(UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2036        u64::from(MACHINE_LIFECYCLE_STORE_RECORD_VERSION),
2037    ];
2038    if !supported.contains(&record_version) {
2039        return MachineLifecycleObservation::Unsupported {
2040            record_version,
2041            evidence_digest,
2042            version,
2043        };
2044    }
2045
2046    let decoded = if record_version == u64::from(MACHINE_LIFECYCLE_STORE_RECORD_VERSION) {
2047        decode_machine_lifecycle_observation_v4(bytes)
2048    } else {
2049        decode_machine_lifecycle_store_record(bytes).map(|snapshot| {
2050            decoded_machine_lifecycle_from_snapshot(record_version as u16, snapshot)
2051        })
2052    };
2053    match decoded {
2054        Ok(record) => MachineLifecycleObservation::Decoded { record, version },
2055        Err(error) => MachineLifecycleObservation::Malformed {
2056            record_version: Some(record_version),
2057            evidence_digest,
2058            version,
2059            detail: error.to_string(),
2060        },
2061    }
2062}
2063
2064fn replacement_repair_blocked(
2065    evidence_digest: Option<String>,
2066    detail: impl Into<String>,
2067) -> RuntimeStoreError {
2068    RuntimeStoreError::MachineLifecycleRepairBlocked {
2069        evidence_digest,
2070        detail: detail.into(),
2071    }
2072}
2073
2074/// Validate whether an exact lifecycle observation may be normalized.
2075///
2076/// Binding, fence, generation, and run atoms describe the dead process that
2077/// authored the observed row; they are not a durable high-water authority and
2078/// may be cleared by an exact-version cold-normalization CAS. Unsupported and
2079/// malformed rows remain fail-closed because this slice cannot prove their
2080/// custody fields safe to preserve.
2081fn validate_machine_lifecycle_replacement(
2082    current: &MachineLifecycleObservation,
2083    _current_raw: Option<&[u8]>,
2084    _replacement: &MachineLifecycleSnapshot,
2085) -> Result<(), RuntimeStoreError> {
2086    match current {
2087        MachineLifecycleObservation::Missing | MachineLifecycleObservation::Decoded { .. } => {
2088            Ok(())
2089        }
2090        MachineLifecycleObservation::Unsupported {
2091            evidence_digest,
2092            record_version,
2093            ..
2094        } => Err(replacement_repair_blocked(
2095            Some(evidence_digest.clone()),
2096            format!(
2097                "unsupported lifecycle record version {record_version} cannot prove fencing semantics"
2098            ),
2099        )),
2100        MachineLifecycleObservation::Malformed {
2101            evidence_digest,
2102            detail,
2103            ..
2104        } => Err(replacement_repair_blocked(
2105            Some(evidence_digest.clone()),
2106            format!("malformed lifecycle evidence is not reclaimable: {detail}"),
2107        )),
2108    }
2109}
2110
2111struct PreparedMachineLifecycleReplacement {
2112    snapshot: MachineLifecycleSnapshot,
2113    bytes: Vec<u8>,
2114    version: MachineLifecycleObservationVersion,
2115}
2116
2117impl PreparedMachineLifecycleReplacement {
2118    /// A runtime-authority normalization owns only lifecycle, binding, and run
2119    /// atoms. Preserve independent supervisor and unregister custody from the
2120    /// exact decoded row rather than copying it through the reconciler.
2121    fn preserve_observed_custody(
2122        mut self,
2123        current: &MachineLifecycleObservation,
2124    ) -> Result<Self, RuntimeStoreError> {
2125        if let MachineLifecycleObservation::Decoded { record, .. } = current {
2126            self.snapshot.supervisor_authority = record.supervisor_authority().clone();
2127            self.snapshot.unregister_progress = record.unregister_progress().cloned();
2128            self.bytes = MachineLifecycleStoreRecord::from_snapshot(&self.snapshot).encode()?;
2129            self.version = MachineLifecycleObservationVersion::from_raw_record(&self.bytes);
2130        }
2131        Ok(self)
2132    }
2133}
2134
2135fn prepare_machine_lifecycle_replacement(
2136    commit: MachineLifecycleCommit,
2137) -> Result<PreparedMachineLifecycleReplacement, RuntimeStoreError> {
2138    let bytes = commit.store_record().encode()?;
2139    let version = MachineLifecycleObservationVersion::from_raw_record(&bytes);
2140    Ok(PreparedMachineLifecycleReplacement {
2141        snapshot: commit.into_snapshot(),
2142        bytes,
2143        version,
2144    })
2145}
2146
2147fn decoded_prepared_machine_lifecycle_replacement(
2148    replacement: &PreparedMachineLifecycleReplacement,
2149) -> Result<DecodedMachineLifecycleObservation, RuntimeStoreError> {
2150    match classify_machine_lifecycle_record(&replacement.bytes) {
2151        MachineLifecycleObservation::Decoded { record, .. } => Ok(record),
2152        other => Err(RuntimeStoreError::Internal(format!(
2153            "machine-authorized lifecycle replacement did not decode: {other:?}"
2154        ))),
2155    }
2156}
2157
2158/// Load the last persisted runtime-state projection from a generated lifecycle
2159/// record.
2160///
2161/// This is a projection of [`MachineLifecycleCommit`] authority. Store
2162/// implementations provide only opaque record bytes; the runtime crate owns the
2163/// decoding and rejects compatibility rows that are not machine lifecycle
2164/// records.
2165pub async fn load_runtime_state(
2166    store: &dyn RuntimeStore,
2167    runtime_id: &LogicalRuntimeId,
2168) -> Result<Option<RuntimeState>, RuntimeStoreError> {
2169    Ok(load_machine_lifecycle(store, runtime_id)
2170        .await?
2171        .map(|snapshot| snapshot.runtime_state()))
2172}
2173
2174pub(crate) async fn load_machine_lifecycle(
2175    store: &dyn RuntimeStore,
2176    runtime_id: &LogicalRuntimeId,
2177) -> Result<Option<MachineLifecycleSnapshot>, RuntimeStoreError> {
2178    store
2179        .load_machine_lifecycle_record(runtime_id)
2180        .await?
2181        .map(|bytes| decode_machine_lifecycle_store_record(&bytes))
2182        .transpose()
2183}
2184
2185/// Declared durable store record for generated machine lifecycle truth.
2186///
2187/// Stores receive this record from [`MachineLifecycleCommit`] and may persist
2188/// its encoded form. Loading must decode this exact record shape; compatibility
2189/// runtime-state projections are not lifecycle authority.
2190#[derive(Debug, Clone, PartialEq, Eq)]
2191pub struct MachineLifecycleStoreRecord {
2192    snapshot: MachineLifecycleSnapshot,
2193}
2194
2195impl MachineLifecycleStoreRecord {
2196    pub(crate) fn from_snapshot(snapshot: &MachineLifecycleSnapshot) -> Self {
2197        Self {
2198            snapshot: snapshot.clone(),
2199        }
2200    }
2201
2202    pub fn encode(&self) -> Result<Vec<u8>, RuntimeStoreError> {
2203        validate_supervisor_authority_snapshot(self.snapshot.supervisor_authority())
2204            .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
2205        validate_unregister_progress_snapshot(self.snapshot.unregister_progress())
2206            .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
2207        let wire = MachineLifecycleSnapshotStoreWire::from(&self.snapshot);
2208        serde_json::to_vec(&wire).map_err(|err| RuntimeStoreError::WriteFailed(err.to_string()))
2209    }
2210}
2211
2212/// Machine-owned lifecycle commit token.
2213///
2214/// This token has no public constructor. RuntimeStore implementors can persist
2215/// the selected state and binding facts, but callers outside the machine/driver
2216/// commit path cannot select arbitrary lifecycle truth.
2217#[derive(Debug, Clone, PartialEq, Eq)]
2218pub struct MachineLifecycleCommit {
2219    snapshot: MachineLifecycleSnapshot,
2220}
2221
2222impl MachineLifecycleCommit {
2223    #[cfg(test)]
2224    pub(crate) fn new_with_binding(
2225        runtime_state: RuntimeState,
2226        binding: MachineLifecycleBindingFacts,
2227        supervisor_authority: SupervisorAuthoritySnapshot,
2228    ) -> Self {
2229        Self::new_with_binding_and_unregister_progress(
2230            runtime_state,
2231            binding,
2232            supervisor_authority,
2233            None,
2234        )
2235    }
2236
2237    pub(crate) fn new_with_binding_and_unregister_progress(
2238        runtime_state: RuntimeState,
2239        binding: MachineLifecycleBindingFacts,
2240        supervisor_authority: SupervisorAuthoritySnapshot,
2241        unregister_progress: Option<MachineUnregisterProgressSnapshot>,
2242    ) -> Self {
2243        Self::new_with_binding_run_and_unregister_progress(
2244            runtime_state,
2245            binding,
2246            MachineLifecycleRunFacts::default(),
2247            supervisor_authority,
2248            unregister_progress,
2249        )
2250    }
2251
2252    pub(crate) fn new_with_binding_run_and_unregister_progress(
2253        runtime_state: RuntimeState,
2254        binding: MachineLifecycleBindingFacts,
2255        run: MachineLifecycleRunFacts,
2256        supervisor_authority: SupervisorAuthoritySnapshot,
2257        unregister_progress: Option<MachineUnregisterProgressSnapshot>,
2258    ) -> Self {
2259        Self {
2260            snapshot: MachineLifecycleSnapshot::new_with_run_and_unregister_progress(
2261                runtime_state,
2262                binding,
2263                run,
2264                supervisor_authority,
2265                unregister_progress,
2266            ),
2267        }
2268    }
2269
2270    /// Runtime state selected by the owning MeerkatMachine transition.
2271    pub fn runtime_state(&self) -> RuntimeState {
2272        self.snapshot.runtime_state()
2273    }
2274
2275    /// Durable lifecycle snapshot selected by the owning MeerkatMachine transition.
2276    pub fn snapshot(&self) -> &MachineLifecycleSnapshot {
2277        &self.snapshot
2278    }
2279
2280    /// Durable record selected by the owning MeerkatMachine transition.
2281    pub fn store_record(&self) -> MachineLifecycleStoreRecord {
2282        MachineLifecycleStoreRecord::from_snapshot(&self.snapshot)
2283    }
2284
2285    pub(crate) fn into_snapshot(self) -> MachineLifecycleSnapshot {
2286        self.snapshot
2287    }
2288}
2289
2290/// Machine-authorized final-unregister persistence token.
2291///
2292/// The token bundles terminal lifecycle truth with the exact authorized input
2293/// snapshot. It has no public constructor and can only be minted by consuming
2294/// the private-field delete witness derived from the generated
2295/// `DeleteSnapshot` unregister verdict.
2296#[derive(Debug, Clone)]
2297pub struct UnregisterFinalizationCommit {
2298    machine_lifecycle: MachineLifecycleCommit,
2299    input_states: Vec<InputStatePersistenceRecord>,
2300    retired_ops_epoch: meerkat_core::RuntimeEpochId,
2301}
2302
2303impl UnregisterFinalizationCommit {
2304    pub(crate) fn new(
2305        machine_lifecycle: MachineLifecycleCommit,
2306        input_states: Vec<InputStatePersistenceRecord>,
2307        retired_ops_epoch: meerkat_core::RuntimeEpochId,
2308        _authority: crate::meerkat_machine::DeleteOpsFinalizationAuthority,
2309    ) -> Self {
2310        Self {
2311            machine_lifecycle,
2312            input_states,
2313            retired_ops_epoch,
2314        }
2315    }
2316
2317    pub(crate) fn into_parts(
2318        self,
2319    ) -> (
2320        MachineLifecycleSnapshot,
2321        Vec<InputStatePersistenceRecord>,
2322        meerkat_core::RuntimeEpochId,
2323    ) {
2324        (
2325            self.machine_lifecycle.into_snapshot(),
2326            self.input_states,
2327            self.retired_ops_epoch,
2328        )
2329    }
2330
2331    /// Opaque encoded lifecycle record selected by final-unregister machine
2332    /// authority. External stores can persist this without gaining a way to
2333    /// construct or alter the authority token.
2334    pub fn lifecycle_store_record(&self) -> MachineLifecycleStoreRecord {
2335        self.machine_lifecycle.store_record()
2336    }
2337
2338    /// Authorized input-state rows that must commit in the same transaction.
2339    pub fn input_states(&self) -> &[InputStatePersistenceRecord] {
2340        &self.input_states
2341    }
2342
2343    /// Exact ops epoch retired by this finalization transaction.
2344    pub fn retired_ops_epoch(&self) -> &meerkat_core::RuntimeEpochId {
2345        &self.retired_ops_epoch
2346    }
2347}
2348
2349/// Atomic persistence interface for runtime state.
2350///
2351/// Implementations:
2352/// - `InMemoryRuntimeStore` — in-memory, no durability (ephemeral/testing)
2353/// - `SqliteRuntimeStore` — SQLite-backed durable runtime state
2354///
2355/// A store may contain many logical runtime ids, but each id is controlled by
2356/// one live `MeerkatMachine` authority. Store transactions provide durable
2357/// atomicity; they are not a distributed lease for two machines concurrently
2358/// controlling the same logical runtime.
2359#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2360#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2361pub trait RuntimeStore: Send + Sync {
2362    /// Whether [`RuntimeStore::atomic_apply`] durably records typed compaction
2363    /// projection intents in the same boundary as the session rewrite.
2364    /// Unknown/custom stores fail closed by default.
2365    fn supports_compaction_projection_outbox(&self) -> bool {
2366        false
2367    }
2368
2369    /// Stable key for process-local auth/OAuth authority reuse across reopened
2370    /// handles for the same durable store.
2371    fn auth_authority_key(&self) -> Option<String> {
2372        None
2373    }
2374
2375    /// Persist the runtime-owned OAuth login-flow payload snapshot.
2376    ///
2377    /// The AuthMachine owns admission/consume semantics; this payload snapshot
2378    /// carries the PKCE verifier and device-code correlation data needed to
2379    /// rehydrate active flows after a persistent runtime process restart.
2380    fn persist_auth_oauth_flow_snapshot(
2381        &self,
2382        snapshot_json: &[u8],
2383    ) -> Result<(), RuntimeStoreError> {
2384        let _ = snapshot_json;
2385        Err(RuntimeStoreError::Unsupported(
2386            "persist_auth_oauth_flow_snapshot".into(),
2387        ))
2388    }
2389
2390    /// Load the runtime-owned OAuth login-flow payload snapshot, if present.
2391    fn load_auth_oauth_flow_snapshot(&self) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
2392        Err(RuntimeStoreError::Unsupported(
2393            "load_auth_oauth_flow_snapshot".into(),
2394        ))
2395    }
2396
2397    /// Atomically update the runtime-owned OAuth login-flow payload snapshot.
2398    ///
2399    /// Stores that support OAuth snapshots must override this with a lock,
2400    /// transaction, or compare-and-swap boundary. A load/compute/persist
2401    /// fallback is not safe for admission, capacity, or consume claims.
2402    fn update_auth_oauth_flow_snapshot(
2403        &self,
2404        _update: &mut AuthOAuthFlowSnapshotUpdate<'_>,
2405    ) -> Result<(), RuntimeStoreError> {
2406        Err(RuntimeStoreError::Unsupported(
2407            "update_auth_oauth_flow_snapshot".into(),
2408        ))
2409    }
2410
2411    /// Atomically persist a session snapshot that is not a run boundary.
2412    ///
2413    /// Session-control snapshots update durable session authority without
2414    /// producing a [`RunBoundaryReceipt`].
2415    async fn commit_session_snapshot(
2416        &self,
2417        runtime_id: &LogicalRuntimeId,
2418        session_delta: SessionDelta,
2419    ) -> Result<(), RuntimeStoreError>;
2420
2421    /// Atomically persist a same-session transcript rewrite snapshot.
2422    ///
2423    /// Store implementations that support transcript edits must compare the
2424    /// currently persisted session transcript revision with `commit.parent_revision`
2425    /// inside the same lock or transaction that writes `session_delta`.
2426    async fn commit_session_transcript_rewrite_snapshot(
2427        &self,
2428        runtime_id: &LogicalRuntimeId,
2429        session_delta: SessionDelta,
2430        commit: &meerkat_core::TranscriptRewriteCommit,
2431    ) -> Result<(), RuntimeStoreError> {
2432        let _ = (runtime_id, session_delta, commit);
2433        Err(RuntimeStoreError::Unsupported(
2434            "commit_session_transcript_rewrite_snapshot".into(),
2435        ))
2436    }
2437
2438    /// Atomically persist session delta + receipt + input state updates.
2439    ///
2440    /// All three writes MUST commit in a single atomic operation.
2441    /// If any write fails, none should be visible.
2442    /// Atomically persist session delta + receipt + input state updates.
2443    ///
2444    /// All writes MUST commit in a single atomic operation.
2445    /// If `session_store_key` is `Some`, validates that the snapshot belongs
2446    /// to that session and, for stores that physically share a `SessionStore`
2447    /// table, writes that table in the same transaction. Runtime snapshot
2448    /// authority remains keyed only by `runtime_id`; `session_store_key` must
2449    /// not create a raw session UUID runtime alias.
2450    /// Compaction intents must be inserted as pending outbox rows in this same
2451    /// boundary. An intent whose exact outbox identity is already finalized is
2452    /// a stale snapshot replay and must be rejected without mutating any part
2453    /// of the boundary.
2454    async fn atomic_apply(
2455        &self,
2456        runtime_id: &LogicalRuntimeId,
2457        session_delta: Option<SessionDelta>,
2458        receipt: RunBoundaryReceipt,
2459        input_updates: Vec<InputStatePersistenceRecord>,
2460        session_store_key: Option<meerkat_core::types::SessionId>,
2461    ) -> Result<(), RuntimeStoreError>;
2462
2463    /// Load exact compaction projection intents committed by atomic_apply but
2464    /// not yet acknowledged as finalized by the memory store.
2465    async fn load_pending_compaction_projections(
2466        &self,
2467        runtime_id: &LogicalRuntimeId,
2468    ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeStoreError> {
2469        let _ = runtime_id;
2470        Err(RuntimeStoreError::Unsupported(
2471            "load_pending_compaction_projections".to_string(),
2472        ))
2473    }
2474
2475    /// Idempotently acknowledge post-commit memory finalization.
2476    ///
2477    /// The acknowledgement and removal of this exact intent from the
2478    /// authoritative persisted session snapshot MUST occur in one atomic
2479    /// boundary. The finalized outbox row remains as a tombstone so later
2480    /// snapshot writes can reject stale metadata replay.
2481    async fn mark_compaction_projection_finalized(
2482        &self,
2483        runtime_id: &LogicalRuntimeId,
2484        projection: &meerkat_core::CompactionProjectionId,
2485    ) -> Result<(), RuntimeStoreError> {
2486        let _ = (runtime_id, projection);
2487        Err(RuntimeStoreError::Unsupported(
2488            "mark_compaction_projection_finalized".to_string(),
2489        ))
2490    }
2491
2492    /// Atomically persist a failed-but-applied runtime turn.
2493    ///
2494    /// This is the machine-terminal counterpart to [`Self::atomic_apply`]:
2495    /// the mutated session snapshot, boundary receipt, generated machine
2496    /// lifecycle record, and input/outbox state must become visible in one
2497    /// transaction. Implementations must never compose this from separate
2498    /// `atomic_apply` and `commit_machine_lifecycle` calls.
2499    async fn atomic_apply_with_machine_lifecycle(
2500        &self,
2501        runtime_id: &LogicalRuntimeId,
2502        session_delta: SessionDelta,
2503        receipt: RunBoundaryReceipt,
2504        machine_lifecycle: MachineLifecycleCommit,
2505        input_updates: Vec<InputStatePersistenceRecord>,
2506        session_store_key: meerkat_core::types::SessionId,
2507    ) -> Result<(), RuntimeStoreError> {
2508        let _ = (
2509            runtime_id,
2510            session_delta,
2511            receipt,
2512            machine_lifecycle,
2513            input_updates,
2514            session_store_key,
2515        );
2516        Err(RuntimeStoreError::Unsupported(
2517            "atomic_apply_with_machine_lifecycle".to_string(),
2518        ))
2519    }
2520
2521    /// Load all input states for a runtime.
2522    async fn load_input_states(
2523        &self,
2524        runtime_id: &LogicalRuntimeId,
2525    ) -> Result<Vec<StoredInputState>, RuntimeStoreError>;
2526
2527    /// Load a specific boundary receipt.
2528    async fn load_boundary_receipt(
2529        &self,
2530        runtime_id: &LogicalRuntimeId,
2531        run_id: &RunId,
2532        sequence: u64,
2533    ) -> Result<Option<RunBoundaryReceipt>, RuntimeStoreError>;
2534
2535    /// Load the latest committed session snapshot for a runtime, if any.
2536    async fn load_session_snapshot(
2537        &self,
2538        runtime_id: &LogicalRuntimeId,
2539    ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
2540
2541    /// Remove the latest committed session snapshot for a runtime.
2542    ///
2543    /// This is used only as a fail-closed quarantine path when transcript
2544    /// rewrite audit failure makes the runtime snapshot itself invalid recovery
2545    /// authority and the service cannot restore the previous snapshot. An
2546    /// ordinary downstream compatibility-projection failure must retain the
2547    /// already-committed runtime snapshot for retry.
2548    async fn clear_session_snapshot(
2549        &self,
2550        runtime_id: &LogicalRuntimeId,
2551    ) -> Result<(), RuntimeStoreError>;
2552
2553    /// Replace the latest committed session snapshot only if it still matches
2554    /// `expected_current`.
2555    ///
2556    /// Used by fail-closed recovery when a rejected transcript-rewrite snapshot
2557    /// must be restored to its prior audited value. Implementations must compare
2558    /// and write atomically so recovery cannot overwrite newer runtime authority.
2559    async fn replace_session_snapshot_if_current(
2560        &self,
2561        runtime_id: &LogicalRuntimeId,
2562        expected_current: &[u8],
2563        replacement: Vec<u8>,
2564    ) -> Result<bool, RuntimeStoreError>;
2565
2566    /// Remove the latest committed session snapshot only if it still matches
2567    /// `expected_current`.
2568    ///
2569    /// This is the conditional variant of the fail-closed quarantine path.
2570    async fn clear_session_snapshot_if_current(
2571        &self,
2572        runtime_id: &LogicalRuntimeId,
2573        expected_current: &[u8],
2574    ) -> Result<bool, RuntimeStoreError>;
2575
2576    /// Report whether the runtime-projection fallback for `runtime_id` is
2577    /// quarantined.
2578    ///
2579    /// This is a durable single-owner fact: when
2580    /// [`clear_session_snapshot_if_current`](Self::clear_session_snapshot_if_current)
2581    /// matches and DELETEs a rejected runtime snapshot, the same atomic boundary
2582    /// records the quarantine marker. A subsequent live snapshot write clears it.
2583    /// Recovery reads this to decide whether a store-only projection may stand in
2584    /// for an absent runtime snapshot. The default is fail-safe (`false`): stores
2585    /// that cannot record the marker durably never claim a snapshot is
2586    /// quarantined.
2587    async fn is_runtime_projection_quarantined(
2588        &self,
2589        runtime_id: &LogicalRuntimeId,
2590    ) -> Result<bool, RuntimeStoreError> {
2591        let _ = runtime_id;
2592        Ok(false)
2593    }
2594
2595    /// Persist a single input state (for durable-before-ack).
2596    async fn persist_input_state(
2597        &self,
2598        runtime_id: &LogicalRuntimeId,
2599        state: &InputStatePersistenceRecord,
2600    ) -> Result<(), RuntimeStoreError>;
2601
2602    /// Atomically persist a batch of machine-authorized input shell updates.
2603    /// Used by per-input terminal outboxes so an N-input batch can never
2604    /// expose a mixed provisional/finalized or finalized/published phase.
2605    async fn persist_input_states_atomically(
2606        &self,
2607        _runtime_id: &LogicalRuntimeId,
2608        states: &[InputStatePersistenceRecord],
2609    ) -> Result<(), RuntimeStoreError> {
2610        if states.is_empty() {
2611            return Ok(());
2612        }
2613        Err(RuntimeStoreError::Unsupported(
2614            "persist_input_states_atomically".to_string(),
2615        ))
2616    }
2617
2618    /// Atomically replace an exact set of input-state rows only when every
2619    /// currently persisted row is byte-identical to its expected
2620    /// [`StoredInputState`] serialization.
2621    ///
2622    /// Expected and replacement batches must contain the same unique keys and
2623    /// at most [`MAX_INPUT_STATE_BATCH_CAS`] rows. If every current row already
2624    /// equals its replacement, implementations return
2625    /// [`InputStateBatchCasOutcome::Swapped`] without rewriting it; this makes
2626    /// a committed store-first transaction retryable after caller
2627    /// cancellation or acknowledgement loss. Missing rows, mixed
2628    /// expected/replacement images, and any other changed durable rows return
2629    /// [`InputStateBatchCasOutcome::Stale`] without writing a replacement.
2630    /// Implementations must hold one lock/transaction across the complete
2631    /// comparison and write set.
2632    async fn compare_and_swap_input_states_atomically(
2633        &self,
2634        _runtime_id: &LogicalRuntimeId,
2635        expected: &[StoredInputState],
2636        replacements: &[InputStatePersistenceRecord],
2637    ) -> Result<InputStateBatchCasOutcome, RuntimeStoreError> {
2638        let prepared = prepare_input_state_batch_cas(expected, replacements)?;
2639        if prepared.is_empty() {
2640            return Ok(InputStateBatchCasOutcome::Swapped);
2641        }
2642        Err(RuntimeStoreError::Unsupported(
2643            "compare_and_swap_input_states_atomically".to_string(),
2644        ))
2645    }
2646
2647    /// Atomically replace an exact input-state batch while an external
2648    /// authority fence is held across the target write.
2649    ///
2650    /// Implementations must compare the target rows first, then retain both
2651    /// the target transaction and the external authority guard until every
2652    /// replacement is committed. This is the cold-registration recovery seam:
2653    /// a process whose lease expires or is superseded must never overwrite
2654    /// input work recovered by its successor.
2655    async fn compare_and_swap_input_states_atomically_with_fence(
2656        &self,
2657        runtime_id: &LogicalRuntimeId,
2658        expected: &[StoredInputState],
2659        replacements: &[InputStatePersistenceRecord],
2660        write_fence: std::sync::Arc<dyn RuntimeStoreWriteFence>,
2661    ) -> Result<FencedInputStateBatchCasOutcome, RuntimeStoreError> {
2662        let prepared = prepare_input_state_batch_cas(expected, replacements)?;
2663        if prepared.is_empty() {
2664            return Ok(FencedInputStateBatchCasOutcome::Swapped);
2665        }
2666        let _ = (runtime_id, write_fence);
2667        Err(RuntimeStoreError::Unsupported(
2668            "compare_and_swap_input_states_atomically_with_fence".to_string(),
2669        ))
2670    }
2671
2672    /// Load a single input state.
2673    async fn load_input_state(
2674        &self,
2675        runtime_id: &LogicalRuntimeId,
2676        input_id: &InputId,
2677    ) -> Result<Option<StoredInputState>, RuntimeStoreError>;
2678
2679    /// Observe one physical machine-lifecycle row without collapsing corrupt
2680    /// or future-version bytes into absence.
2681    ///
2682    /// This is the recovery/reconciliation read surface. Custom stores must
2683    /// implement it explicitly; the default is capability-unavailable rather
2684    /// than inferring a total observation from the older strict decoder.
2685    async fn observe_machine_lifecycle(
2686        &self,
2687        runtime_id: &LogicalRuntimeId,
2688    ) -> Result<MachineLifecycleObservation, RuntimeStoreError> {
2689        let _ = runtime_id;
2690        Err(RuntimeStoreError::Unsupported(
2691            "observe_machine_lifecycle".to_string(),
2692        ))
2693    }
2694
2695    /// Replace exactly one machine-lifecycle row when it is absent or still
2696    /// has the observed raw-content version.
2697    ///
2698    /// Built-in stores atomically compare the raw-content version and publish
2699    /// the machine-authorized replacement. Binding, generation, fence, and
2700    /// run atoms belong to the dead process that wrote the observed row; they
2701    /// are not durable high-waters and may be cleared by an exact-version
2702    /// cold-normalization CAS. The caller retains the prior raw digest for
2703    /// output-only diagnostics. Conflicts are ordinary level-triggered
2704    /// re-observation; unsupported or malformed bytes return
2705    /// [`RuntimeStoreError::MachineLifecycleRepairBlocked`].
2706    async fn compare_and_swap_machine_lifecycle(
2707        &self,
2708        runtime_id: &LogicalRuntimeId,
2709        expected: MachineLifecycleExpectedVersion,
2710        replacement: MachineLifecycleCommit,
2711    ) -> Result<MachineLifecycleCasOutcome, RuntimeStoreError> {
2712        let _ = (runtime_id, expected, replacement);
2713        Err(RuntimeStoreError::Unsupported(
2714            "compare_and_swap_machine_lifecycle".to_string(),
2715        ))
2716    }
2717
2718    /// Replace exactly one lifecycle row while an external authority fence is
2719    /// held across the target write.
2720    ///
2721    /// This is the conditional-registration store seam. Built-in stores call
2722    /// `write_fence` inside the row lock/transaction after the exact raw-row
2723    /// comparison and immediately before publication. Custom stores must opt
2724    /// in explicitly; the default is capability-unavailable rather than an
2725    /// unfenced fallback to compare_and_swap_machine_lifecycle.
2726    async fn compare_and_swap_machine_lifecycle_with_fence(
2727        &self,
2728        runtime_id: &LogicalRuntimeId,
2729        expected: MachineLifecycleExpectedVersion,
2730        replacement: MachineLifecycleCommit,
2731        write_fence: std::sync::Arc<dyn RuntimeStoreWriteFence>,
2732    ) -> Result<FencedMachineLifecycleCasOutcome, RuntimeStoreError> {
2733        let _ = (runtime_id, expected, replacement, write_fence);
2734        Err(RuntimeStoreError::Unsupported(
2735            "compare_and_swap_machine_lifecycle_with_fence".to_string(),
2736        ))
2737    }
2738
2739    /// Load the last persisted machine lifecycle record bytes, if any.
2740    ///
2741    /// Implementations return only the opaque bytes previously obtained from
2742    /// [`MachineLifecycleCommit::store_record`]. The runtime crate decodes
2743    /// these bytes through `load_runtime_state` or internal recovery helpers;
2744    /// stores must not promote compatibility rows or bare runtime states into
2745    /// lifecycle authority.
2746    async fn load_machine_lifecycle_record(
2747        &self,
2748        runtime_id: &LogicalRuntimeId,
2749    ) -> Result<Option<Vec<u8>>, RuntimeStoreError>;
2750
2751    /// Atomically commit machine-owned lifecycle state changes.
2752    ///
2753    /// Writes runtime state, generated runtime binding facts, and all input
2754    /// state updates in a single atomic operation. `MachineLifecycleCommit` has
2755    /// no public constructor, so this cannot be used by compatibility callers
2756    /// to pick runtime truth.
2757    async fn commit_machine_lifecycle(
2758        &self,
2759        runtime_id: &LogicalRuntimeId,
2760        commit: MachineLifecycleCommit,
2761        input_states: &[InputStatePersistenceRecord],
2762    ) -> Result<(), RuntimeStoreError>;
2763
2764    /// Atomically publish final unregister lifecycle truth and retire the
2765    /// matching ops-lifecycle epoch.
2766    ///
2767    /// The lifecycle record, input-state updates, and ops snapshot deletion
2768    /// MUST commit in one store transaction (or one indivisible in-memory
2769    /// critical section). A terminal lifecycle record with the old ops epoch
2770    /// still present is forbidden: recovery would otherwise resurrect stale
2771    /// operation/cursor authority after unregister. The commit also carries
2772    /// the exact retired ops epoch; implementations MUST atomically retain a
2773    /// durable deletion-wins fence for it, and every later
2774    /// `persist_ops_lifecycle` for that epoch must return
2775    /// [`RuntimeStoreError::OpsLifecycleEpochRetired`] rather than recreate the
2776    /// row. Implementations must also be idempotent so retry after a process
2777    /// crash following commit converges on the same terminal lifecycle with no
2778    /// ops snapshot and the same epoch fence.
2779    ///
2780    /// `Ok(())` means the whole finalization is visible. Every error except
2781    /// [`RuntimeStoreError::UnregisterFinalizationOutcomeUnknown`] MUST mean
2782    /// none of it is visible. A backend with an ambiguous commit
2783    /// acknowledgement must first resolve that ambiguity internally by
2784    /// reading its transaction authority. It may use the typed unknown error
2785    /// only when it cannot prove either the exact final state or the exact
2786    /// pre-transaction state; callers then retry without a durable rollback.
2787    /// The opaque token also proves the generated `DeleteSnapshot` verdict and
2788    /// bundles the exact lifecycle and input rows selected by the machine.
2789    ///
2790    /// The returned future is also a cancellation boundary: after it is
2791    /// dropped, no mutation from that invocation may become visible later.
2792    /// An implementation may leave the prior pair untouched or finish the
2793    /// entire atomic commit before cancellation is observable, but it must not
2794    /// detach a background write that can cross a same-runtime-ID replacement.
2795    async fn commit_unregister_finalization(
2796        &self,
2797        runtime_id: &LogicalRuntimeId,
2798        finalization: UnregisterFinalizationCommit,
2799    ) -> Result<(), RuntimeStoreError> {
2800        let _ = (runtime_id, finalization);
2801        Err(RuntimeStoreError::Unsupported(
2802            "commit_unregister_finalization".into(),
2803        ))
2804    }
2805
2806    /// Atomically initialize the ops lifecycle row if it is absent and return
2807    /// the canonical durable snapshot.
2808    ///
2809    /// The absence check, optional insert, and canonical read MUST share one
2810    /// store transaction (or one indivisible in-memory critical section).
2811    /// Concurrent initializer calls for the same runtime must therefore all
2812    /// observe the same epoch: exactly one candidate may become durable and
2813    /// every losing caller receives that winner's snapshot. The machine's
2814    /// stable registration transaction separately spans this store call
2815    /// through map publication/removal; this method is not a distributed
2816    /// machine lease. Implementations must also reject a candidate whose epoch
2817    /// is already covered by the unregister deletion-wins fence.
2818    ///
2819    /// Cancellation may leave the candidate as the canonical empty row: no
2820    /// bindings escape before this await completes, and the next registrar
2821    /// adopts the returned durable epoch. A cancelled invocation must never
2822    /// overwrite a row that was already present.
2823    ///
2824    /// There is intentionally no load-then-persist default. Custom stores
2825    /// that support durable ops lifecycle state must implement this atomic
2826    /// boundary or fail closed with [`RuntimeStoreError::Unsupported`].
2827    async fn initialize_ops_lifecycle_if_absent(
2828        &self,
2829        runtime_id: &LogicalRuntimeId,
2830        candidate: &crate::ops_lifecycle::PersistedOpsSnapshot,
2831    ) -> Result<crate::ops_lifecycle::PersistedOpsSnapshot, RuntimeStoreError> {
2832        let _ = (runtime_id, candidate);
2833        Err(RuntimeStoreError::Unsupported(
2834            "initialize_ops_lifecycle_if_absent".into(),
2835        ))
2836    }
2837
2838    /// Persist a snapshot of the ops lifecycle registry state.
2839    async fn persist_ops_lifecycle(
2840        &self,
2841        runtime_id: &LogicalRuntimeId,
2842        snapshot: &crate::ops_lifecycle::PersistedOpsSnapshot,
2843    ) -> Result<(), RuntimeStoreError> {
2844        let _ = (runtime_id, snapshot);
2845        Err(RuntimeStoreError::Unsupported(
2846            "persist_ops_lifecycle".into(),
2847        ))
2848    }
2849
2850    /// Load a previously persisted ops lifecycle snapshot.
2851    async fn load_ops_lifecycle(
2852        &self,
2853        runtime_id: &LogicalRuntimeId,
2854    ) -> Result<Option<crate::ops_lifecycle::PersistedOpsSnapshot>, RuntimeStoreError> {
2855        let _ = runtime_id;
2856        Err(RuntimeStoreError::Unsupported("load_ops_lifecycle".into()))
2857    }
2858
2859    /// Delete a previously persisted ops lifecycle snapshot.
2860    async fn delete_ops_lifecycle(
2861        &self,
2862        runtime_id: &LogicalRuntimeId,
2863    ) -> Result<(), RuntimeStoreError> {
2864        let _ = runtime_id;
2865        Err(RuntimeStoreError::Unsupported(
2866            "delete_ops_lifecycle".into(),
2867        ))
2868    }
2869
2870    // -----------------------------------------------------------------------
2871    // Mob host binding rows (`runtime_mob_host_bindings`, multi-host mobs R8)
2872    // -----------------------------------------------------------------------
2873    //
2874    // Raw record-JSON accessors only: the TYPED record and the
2875    // transition-derived persistence authorities live mob-side
2876    // (`meerkat-mob/src/runtime/host_actor.rs`); this store never interprets
2877    // the blob. CAS compares the full serialized record, mirroring the
2878    // `mob_runtime_supervisors` mechanics.
2879
2880    /// Load the persisted host-binding record blob for `mob_id`, if any.
2881    async fn load_mob_host_binding(
2882        &self,
2883        mob_id: &str,
2884    ) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
2885        let _ = mob_id;
2886        Err(RuntimeStoreError::Unsupported(
2887            "load_mob_host_binding".into(),
2888        ))
2889    }
2890
2891    /// List every persisted host-binding row (boot recovery).
2892    async fn list_mob_host_bindings(&self) -> Result<Vec<(String, Vec<u8>)>, RuntimeStoreError> {
2893        Err(RuntimeStoreError::Unsupported(
2894            "list_mob_host_bindings".into(),
2895        ))
2896    }
2897
2898    /// Insert the host-binding row for `mob_id` iff absent. Returns whether
2899    /// the row was inserted.
2900    async fn put_mob_host_binding_if_absent(
2901        &self,
2902        mob_id: &str,
2903        record_json: &[u8],
2904    ) -> Result<bool, RuntimeStoreError> {
2905        let _ = (mob_id, record_json);
2906        Err(RuntimeStoreError::Unsupported(
2907            "put_mob_host_binding_if_absent".into(),
2908        ))
2909    }
2910
2911    /// Replace the host-binding row for `mob_id` iff the stored blob equals
2912    /// `expected_json`. Returns whether the swap applied.
2913    async fn compare_and_put_mob_host_binding(
2914        &self,
2915        mob_id: &str,
2916        expected_json: &[u8],
2917        next_json: &[u8],
2918    ) -> Result<bool, RuntimeStoreError> {
2919        let _ = (mob_id, expected_json, next_json);
2920        Err(RuntimeStoreError::Unsupported(
2921            "compare_and_put_mob_host_binding".into(),
2922        ))
2923    }
2924
2925    /// Delete the host-binding row for `mob_id` iff the stored blob equals
2926    /// `expected_json`. Returns whether a row was deleted.
2927    async fn delete_mob_host_binding(
2928        &self,
2929        mob_id: &str,
2930        expected_json: &[u8],
2931    ) -> Result<bool, RuntimeStoreError> {
2932        let _ = (mob_id, expected_json);
2933        Err(RuntimeStoreError::Unsupported(
2934            "delete_mob_host_binding".into(),
2935        ))
2936    }
2937
2938    /// Load the durable receipt for an already-completed host revocation.
2939    ///
2940    /// The blob is deliberately separate from `runtime_mob_host_bindings`:
2941    /// boot recovery must never mistake a revoke retry receipt for a live
2942    /// binding or revive the materialized-member rows that the revoke
2943    /// removed. The typed receipt and its transition witness live mob-side;
2944    /// this store treats it as opaque bytes.
2945    async fn load_mob_host_revocation(
2946        &self,
2947        mob_id: &str,
2948    ) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
2949        let _ = mob_id;
2950        Err(RuntimeStoreError::Unsupported(
2951            "load_mob_host_revocation".into(),
2952        ))
2953    }
2954
2955    /// List durable host-revocation receipts for boot recovery of exact
2956    /// reply-loss retries. Receipts are not bindings and carry no member
2957    /// revival rows.
2958    async fn list_mob_host_revocations(&self) -> Result<Vec<(String, Vec<u8>)>, RuntimeStoreError> {
2959        Err(RuntimeStoreError::Unsupported(
2960            "list_mob_host_revocations".into(),
2961        ))
2962    }
2963
2964    /// Atomically delete the expected active binding and publish its revoke
2965    /// receipt. Returns `false` when the expected binding did not match; in
2966    /// that case neither write is visible.
2967    ///
2968    /// This is the durable terminal boundary for host revocation. A crash
2969    /// before it leaves the binding retryable; a crash after it leaves no
2970    /// binding/member rows to revive and an exact receipt to replay.
2971    async fn revoke_mob_host_binding(
2972        &self,
2973        mob_id: &str,
2974        expected_binding_json: &[u8],
2975        receipt_json: &[u8],
2976    ) -> Result<bool, RuntimeStoreError> {
2977        let _ = (mob_id, expected_binding_json, receipt_json);
2978        Err(RuntimeStoreError::Unsupported(
2979            "revoke_mob_host_binding".into(),
2980        ))
2981    }
2982}
2983
2984pub use memory::InMemoryRuntimeStore;
2985#[cfg(feature = "sqlite-store")]
2986pub use sqlite::SqliteRuntimeStore;
2987
2988#[cfg(test)]
2989mod lifecycle_record_compatibility_tests {
2990    use super::*;
2991
2992    fn operation_id(
2993        value: u128,
2994    ) -> meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId {
2995        meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId::from_uuid(
2996            uuid::Uuid::from_u128(value),
2997        )
2998    }
2999
3000    fn binding(seed: u8, name: &str, epoch: u64) -> SupervisorBindingReceipt {
3001        let pubkey = [seed; 32];
3002        SupervisorBindingReceipt::new(
3003            name.to_string(),
3004            meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey).as_str(),
3005            format!("inproc://{name}"),
3006            crate::comms_drain::encode_supervisor_signing_public_key(pubkey),
3007            epoch,
3008        )
3009    }
3010
3011    fn rotation(
3012        operation_id: meerkat_contracts::wire::supervisor_bridge::SupervisorRotationOperationId,
3013        phase: SupervisorRotationPersistencePhase,
3014        rejection: Option<SupervisorRotationRejection>,
3015        previous: SupervisorBindingReceipt,
3016        next: SupervisorBindingReceipt,
3017    ) -> SupervisorRotationReceipt {
3018        SupervisorRotationReceipt::new(operation_id, phase, rejection, previous, next)
3019    }
3020
3021    fn snapshot(authority: SupervisorAuthoritySnapshot) -> MachineLifecycleSnapshot {
3022        MachineLifecycleSnapshot::new(
3023            RuntimeState::Idle,
3024            MachineLifecycleBindingFacts::new(None, None, None, None),
3025            authority,
3026        )
3027    }
3028
3029    fn encode_snapshot(snapshot: &MachineLifecycleSnapshot) -> Vec<u8> {
3030        MachineLifecycleStoreRecord::from_snapshot(snapshot)
3031            .encode()
3032            .expect("encode lifecycle snapshot")
3033    }
3034
3035    fn encode_unvalidated_snapshot(snapshot: &MachineLifecycleSnapshot) -> Vec<u8> {
3036        serde_json::to_vec(&MachineLifecycleSnapshotStoreWire::from(snapshot))
3037            .expect("serialize deliberately corrupt lifecycle snapshot")
3038    }
3039
3040    fn encoded_value(snapshot: &MachineLifecycleSnapshot) -> serde_json::Value {
3041        serde_json::from_slice(&encode_snapshot(snapshot)).expect("decode encoded snapshot as JSON")
3042    }
3043
3044    fn assert_decode_fails(value: serde_json::Value) {
3045        let bytes = serde_json::to_vec(&value).expect("serialize corrupt lifecycle record");
3046        assert!(
3047            decode_machine_lifecycle_store_record(&bytes).is_err(),
3048            "corrupt lifecycle record must fail closed: {value}"
3049        );
3050    }
3051
3052    #[test]
3053    fn version_one_record_without_supervisor_authority_migrates_explicitly_to_unbound() {
3054        let bytes = serde_json::to_vec(&serde_json::json!({
3055            "record_version": LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3056            "runtime_state": RuntimeState::Retired,
3057            "binding": {
3058                "agent_runtime_id": "rt:session:legacy-v1",
3059                "fence_token": 19,
3060                "runtime_generation": 4,
3061                "runtime_epoch_id": "epoch-legacy-v1"
3062            }
3063        }))
3064        .expect("serialize legacy v1 lifecycle record");
3065
3066        let decoded = decode_machine_lifecycle_store_record(&bytes)
3067            .expect("valid v1 record without the additive field must decode");
3068        assert_eq!(decoded.runtime_state(), RuntimeState::Retired);
3069        assert_eq!(
3070            decoded.supervisor_authority(),
3071            &SupervisorAuthoritySnapshot::UnboundNoReceipt
3072        );
3073    }
3074
3075    #[test]
3076    fn current_record_requires_supervisor_authority_and_unregister_progress_presence() {
3077        assert_decode_fails(serde_json::json!({
3078            "record_version": MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3079            "runtime_state": RuntimeState::Idle,
3080            "binding": {
3081                "agent_runtime_id": null,
3082                "fence_token": null,
3083                "runtime_generation": null,
3084                "runtime_epoch_id": null
3085            },
3086            "unregister_progress": null
3087        }));
3088    }
3089
3090    #[test]
3091    fn current_nullable_fields_require_presence_but_accept_explicit_null() {
3092        let unbound = snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt);
3093        let encoded = encoded_value(&unbound);
3094        assert_eq!(
3095            decode_machine_lifecycle_store_record(
3096                &serde_json::to_vec(&encoded).expect("serialize valid current record")
3097            )
3098            .expect("explicit-null current binding fields must decode"),
3099            unbound
3100        );
3101        let mut missing_progress = encoded.clone();
3102        missing_progress
3103            .as_object_mut()
3104            .expect("lifecycle record object")
3105            .remove("unregister_progress");
3106        assert_decode_fails(missing_progress);
3107
3108        for field in [
3109            "agent_runtime_id",
3110            "fence_token",
3111            "runtime_generation",
3112            "runtime_epoch_id",
3113        ] {
3114            let mut partial = encoded.clone();
3115            partial["binding"]
3116                .as_object_mut()
3117                .expect("binding object")
3118                .remove(field);
3119            assert_decode_fails(partial);
3120        }
3121        for field in ["current_run_id", "pre_run_phase"] {
3122            let mut partial = encoded.clone();
3123            partial
3124                .as_object_mut()
3125                .expect("lifecycle record object")
3126                .remove(field);
3127            assert_decode_fails(partial);
3128        }
3129
3130        let completed = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3131            operation_id(101),
3132            SupervisorRotationPersistencePhase::Completed,
3133            None,
3134            binding(30, "required-null-previous", 4),
3135            binding(31, "required-null-next", 5),
3136        )));
3137        let mut missing_rejection = encoded_value(&completed);
3138        assert!(missing_rejection["supervisor_authority"]["rotation"]["rejection"].is_null());
3139        missing_rejection["supervisor_authority"]["rotation"]
3140            .as_object_mut()
3141            .expect("rotation object")
3142            .remove("rejection");
3143        assert_decode_fails(missing_rejection);
3144    }
3145
3146    #[test]
3147    fn lossless_observation_preserves_partial_run_pair_and_nullable_lifecycle() {
3148        let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt));
3149        let run_id = RunId::new();
3150        value["runtime_state"] = serde_json::Value::Null;
3151        value["current_run_id"] = serde_json::to_value(&run_id).expect("serialize run id");
3152        value["pre_run_phase"] = serde_json::Value::Null;
3153        let bytes = serde_json::to_vec(&value).expect("serialize partial lifecycle row");
3154
3155        let MachineLifecycleObservation::Decoded { record, version } =
3156            classify_machine_lifecycle_record(&bytes)
3157        else {
3158            panic!("explicitly nullable partial runtime tuple must remain decoded");
3159        };
3160        assert_eq!(
3161            record.record_version(),
3162            MACHINE_LIFECYCLE_STORE_RECORD_VERSION
3163        );
3164        assert_eq!(record.runtime_state(), None);
3165        assert_eq!(record.run().current_run_id(), Some(&run_id));
3166        assert_eq!(record.run().pre_run_phase(), None);
3167        assert_eq!(
3168            version.as_str(),
3169            format!("sha256:{:x}", Sha256::digest(&bytes))
3170        );
3171        assert!(decode_machine_lifecycle_store_record(&bytes).is_err());
3172    }
3173
3174    #[test]
3175    fn lifecycle_observation_distinguishes_unsupported_and_malformed_raw_rows() {
3176        let unsupported = br#"{"record_version":99,"opaque":"future"}"#;
3177        assert!(matches!(
3178            classify_machine_lifecycle_record(unsupported),
3179            MachineLifecycleObservation::Unsupported {
3180                record_version: 99,
3181                ..
3182            }
3183        ));
3184
3185        let malformed = br#"{"record_version":4,"binding":"torn"}"#;
3186        assert!(matches!(
3187            classify_machine_lifecycle_record(malformed),
3188            MachineLifecycleObservation::Malformed {
3189                record_version: Some(4),
3190                ..
3191            }
3192        ));
3193
3194        let undecodable = b"not-json";
3195        assert!(matches!(
3196            classify_machine_lifecycle_record(undecodable),
3197            MachineLifecycleObservation::Malformed {
3198                record_version: None,
3199                ..
3200            }
3201        ));
3202    }
3203
3204    #[test]
3205    fn version_three_unregister_record_migrates_without_run_binding() {
3206        let expected = snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt);
3207        let mut value = encoded_value(&expected);
3208        value["record_version"] =
3209            serde_json::json!(UNREGISTER_MACHINE_LIFECYCLE_STORE_RECORD_VERSION);
3210        value
3211            .as_object_mut()
3212            .expect("lifecycle record object")
3213            .remove("current_run_id");
3214        value
3215            .as_object_mut()
3216            .expect("lifecycle record object")
3217            .remove("pre_run_phase");
3218        let bytes = serde_json::to_vec(&value).expect("serialize v3 row");
3219        let decoded = decode_machine_lifecycle_store_record(&bytes).expect("decode v3 row");
3220        assert_eq!(decoded, expected);
3221        assert_eq!(decoded.run(), &MachineLifecycleRunFacts::default());
3222    }
3223
3224    #[test]
3225    fn version_two_supervisor_record_migrates_with_no_unregister_progress() {
3226        let bytes = serde_json::to_vec(&serde_json::json!({
3227            "record_version": SUPERVISOR_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3228            "runtime_state": RuntimeState::Retired,
3229            "binding": {
3230                "agent_runtime_id": "rt:session:legacy-v2",
3231                "fence_token": 23,
3232                "runtime_generation": 5,
3233                "runtime_epoch_id": "epoch-legacy-v2"
3234            },
3235            "supervisor_authority": { "kind": "unbound_no_receipt" }
3236        }))
3237        .expect("serialize v2 lifecycle record");
3238
3239        let decoded = decode_machine_lifecycle_store_record(&bytes)
3240            .expect("valid v2 supervisor record must migrate");
3241        assert_eq!(decoded.runtime_state(), RuntimeState::Retired);
3242        assert_eq!(decoded.unregister_progress(), None);
3243    }
3244
3245    #[test]
3246    fn current_unregister_progress_rejects_forced_disposition_before_feedback() {
3247        let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::UnboundNoReceipt));
3248        value["unregister_progress"] = serde_json::json!({
3249            "runtime_loop_drain_pending": true,
3250            "comms_drain_exit_pending": false,
3251            "completion_waiter_drain_pending": true,
3252            "runtime_loop_forced_abort": true,
3253            "comms_drain_forced_abort": false
3254        });
3255        assert_decode_fails(value);
3256    }
3257
3258    #[test]
3259    fn version_one_migration_rejects_current_authority_fields() {
3260        assert_decode_fails(serde_json::json!({
3261            "record_version": LEGACY_MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
3262            "runtime_state": RuntimeState::Idle,
3263            "binding": {
3264                "agent_runtime_id": null,
3265                "fence_token": null,
3266                "runtime_generation": null,
3267                "runtime_epoch_id": null
3268            },
3269            "supervisor_authority": { "kind": "unbound_no_receipt" }
3270        }));
3271    }
3272
3273    #[test]
3274    fn mixed_or_unknown_supervisor_authority_fields_fail_closed() {
3275        let current = binding(1, "current-supervisor", 7);
3276        let mut value = encoded_value(&snapshot(SupervisorAuthoritySnapshot::Bound(current)));
3277        value["supervisor_authority"]["rotation"] = serde_json::json!({});
3278        assert_decode_fails(value);
3279    }
3280
3281    #[test]
3282    fn completed_rotation_operation_receipt_round_trips_for_cold_observation() {
3283        let snapshot = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3284            operation_id(1),
3285            SupervisorRotationPersistencePhase::Completed,
3286            None,
3287            binding(1, "previous-supervisor", 7),
3288            binding(2, "next-supervisor", 8),
3289        )));
3290
3291        let encoded = encode_snapshot(&snapshot);
3292        let decoded = decode_machine_lifecycle_store_record(&encoded)
3293            .expect("decode completed rotation receipt");
3294
3295        assert_eq!(decoded, snapshot);
3296    }
3297
3298    #[test]
3299    fn exact_current_completed_adoption_round_trips_but_other_equal_epoch_completion_fails() {
3300        let current = binding(3, "already-rotated-supervisor", 9);
3301        let adoption = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3302            operation_id(2),
3303            SupervisorRotationPersistencePhase::Completed,
3304            None,
3305            current.clone(),
3306            current,
3307        )));
3308        assert_eq!(
3309            decode_machine_lifecycle_store_record(&encode_snapshot(&adoption))
3310                .expect("exact-current legacy adoption receipt must decode"),
3311            adoption
3312        );
3313
3314        let non_advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3315            operation_id(3),
3316            SupervisorRotationPersistencePhase::Completed,
3317            None,
3318            binding(3, "previous-supervisor", 9),
3319            binding(4, "different-supervisor", 9),
3320        )));
3321        assert!(
3322            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&non_advancing))
3323                .is_err()
3324        );
3325    }
3326
3327    #[test]
3328    fn malformed_rotation_descriptors_epochs_and_operation_ids_fail_closed() {
3329        let invalid_previous = SupervisorBindingReceipt::new(
3330            String::new(),
3331            "not-a-uuid".to_string(),
3332            "not-an-address".to_string(),
3333            "not-a-key".to_string(),
3334            1,
3335        );
3336        let invalid_previous_receipt =
3337            snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3338                operation_id(4),
3339                SupervisorRotationPersistencePhase::Rejected,
3340                Some(SupervisorRotationRejection::InvalidTarget),
3341                invalid_previous,
3342                binding(5, "raw-target", 2),
3343            )));
3344        assert!(
3345            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3346                &invalid_previous_receipt,
3347            ))
3348            .is_err()
3349        );
3350
3351        let invalid_next = SupervisorBindingReceipt::new(
3352            "invalid-target".to_string(),
3353            "not-a-uuid".to_string(),
3354            "not-an-address".to_string(),
3355            "not-a-key".to_string(),
3356            2,
3357        );
3358        let invalid_completed_target =
3359            snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3360                operation_id(5),
3361                SupervisorRotationPersistencePhase::Completed,
3362                None,
3363                binding(6, "previous-supervisor", 1),
3364                invalid_next,
3365            )));
3366        assert!(
3367            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3368                &invalid_completed_target,
3369            ))
3370            .is_err()
3371        );
3372
3373        let mut invalid_id = encoded_value(&snapshot(
3374            SupervisorAuthoritySnapshot::RotationOperation(rotation(
3375                operation_id(6),
3376                SupervisorRotationPersistencePhase::PreviousRevokePending,
3377                None,
3378                binding(7, "previous-supervisor", 1),
3379                binding(8, "next-supervisor", 2),
3380            )),
3381        ));
3382        invalid_id["supervisor_authority"]["rotation"]["operation_id"] =
3383            serde_json::json!("not-a-uuid");
3384        assert_decode_fails(invalid_id);
3385
3386        let nil_id = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3387            operation_id(0),
3388            SupervisorRotationPersistencePhase::PreviousRevokePending,
3389            None,
3390            binding(7, "previous-supervisor", 1),
3391            binding(8, "next-supervisor", 2),
3392        )));
3393        assert!(
3394            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&nil_id)).is_err()
3395        );
3396
3397        let non_advancing_pending =
3398            snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3399                operation_id(13),
3400                SupervisorRotationPersistencePhase::PreviousRevokePending,
3401                None,
3402                binding(7, "previous-supervisor", 4),
3403                binding(8, "next-supervisor", 4),
3404            )));
3405        assert!(
3406            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3407                &non_advancing_pending,
3408            ))
3409            .is_err()
3410        );
3411    }
3412
3413    #[test]
3414    fn rejected_invalid_or_unsupported_target_preserves_raw_evidence() {
3415        for (id, rejection) in [
3416            (7, SupervisorRotationRejection::InvalidTarget),
3417            (14, SupervisorRotationRejection::UnsupportedProtocolVersion),
3418        ] {
3419            let raw_invalid_target = SupervisorBindingReceipt::new(
3420                "".to_string(),
3421                "not-a-peer-id".to_string(),
3422                "not-an-address".to_string(),
3423                "not-a-signing-key".to_string(),
3424                0,
3425            );
3426            let snapshot = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3427                operation_id(id),
3428                SupervisorRotationPersistencePhase::Rejected,
3429                Some(rejection),
3430                binding(9, "retained-supervisor", 11),
3431                raw_invalid_target,
3432            )));
3433            assert_eq!(
3434                decode_machine_lifecycle_store_record(&encode_snapshot(&snapshot))
3435                    .expect("rejected raw target evidence must remain durable"),
3436                snapshot
3437            );
3438        }
3439    }
3440
3441    #[test]
3442    fn only_raw_target_rejections_are_durable_and_epoch_rejection_must_be_genuine() {
3443        for (id, rejection) in [
3444            (102, SupervisorRotationRejection::OperationConflict),
3445            (103, SupervisorRotationRejection::NotBound),
3446            (104, SupervisorRotationRejection::SenderMismatch),
3447        ] {
3448            let impossible = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3449                operation_id(id),
3450                SupervisorRotationPersistencePhase::Rejected,
3451                Some(rejection),
3452                binding(32, "retained-supervisor", 7),
3453                binding(33, "requested-supervisor", 8),
3454            )));
3455            assert!(
3456                MachineLifecycleStoreRecord::from_snapshot(&impossible)
3457                    .encode()
3458                    .is_err()
3459            );
3460            assert!(
3461                decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&impossible))
3462                    .is_err()
3463            );
3464        }
3465
3466        let advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3467            operation_id(105),
3468            SupervisorRotationPersistencePhase::Rejected,
3469            Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
3470            binding(34, "retained-supervisor", 9),
3471            binding(35, "advancing-target", 10),
3472        )));
3473        assert!(
3474            MachineLifecycleStoreRecord::from_snapshot(&advancing)
3475                .encode()
3476                .is_err()
3477        );
3478        assert!(
3479            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&advancing))
3480                .is_err()
3481        );
3482
3483        let non_advancing = snapshot(SupervisorAuthoritySnapshot::RotationOperation(rotation(
3484            operation_id(106),
3485            SupervisorRotationPersistencePhase::Rejected,
3486            Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
3487            binding(36, "retained-supervisor", 11),
3488            binding(37, "non-advancing-target", 11),
3489        )));
3490        assert_eq!(
3491            decode_machine_lifecycle_store_record(&encode_snapshot(&non_advancing))
3492                .expect("genuine target-epoch rejection must remain durable"),
3493            non_advancing
3494        );
3495    }
3496
3497    #[test]
3498    fn malformed_current_authority_variants_fail_closed() {
3499        let malformed = SupervisorBindingReceipt::new(
3500            String::new(),
3501            "not-a-peer-id".to_string(),
3502            "not-an-address".to_string(),
3503            "not-a-signing-key".to_string(),
3504            1,
3505        );
3506        let bound = snapshot(SupervisorAuthoritySnapshot::Bound(malformed.clone()));
3507        assert!(
3508            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&bound)).is_err()
3509        );
3510
3511        let pending = snapshot(SupervisorAuthoritySnapshot::RevocationPending(
3512            SupervisorRevocationPendingReceipt::new(
3513                malformed.name().to_owned(),
3514                malformed.peer_id().to_owned(),
3515                malformed.address().to_owned(),
3516                malformed.signing_public_key().to_owned(),
3517                malformed.epoch(),
3518            ),
3519        ));
3520        assert!(
3521            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&pending)).is_err()
3522        );
3523
3524        let revoked = snapshot(SupervisorAuthoritySnapshot::RevokedReceipt(
3525            RevokedSupervisorReceipt::new(
3526                malformed.peer_id().to_owned(),
3527                malformed.signing_public_key().to_owned(),
3528                malformed.epoch(),
3529            ),
3530        ));
3531        assert!(
3532            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&revoked)).is_err()
3533        );
3534    }
3535
3536    #[test]
3537    fn partial_and_nonterminal_history_records_fail_closed() {
3538        let receipt = rotation(
3539            operation_id(8),
3540            SupervisorRotationPersistencePhase::Completed,
3541            None,
3542            binding(10, "history-previous", 1),
3543            binding(11, "history-next", 2),
3544        );
3545        let history = std::collections::BTreeMap::from([(receipt.operation_id(), receipt)]);
3546        let snapshot = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3547            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3548                12,
3549                "current-supervisor",
3550                3,
3551            ))),
3552            terminal_receipts: history,
3553        });
3554
3555        let mut partial = encoded_value(&snapshot);
3556        partial["supervisor_authority"]["terminal_receipts"][0]
3557            .as_object_mut()
3558            .expect("history receipt object")
3559            .remove("next");
3560        assert_decode_fails(partial);
3561
3562        let mut nonterminal = encoded_value(&snapshot);
3563        nonterminal["supervisor_authority"]["terminal_receipts"][0]["phase"] =
3564            serde_json::json!("next_publish_pending");
3565        assert_decode_fails(nonterminal);
3566    }
3567
3568    #[test]
3569    fn duplicate_nested_and_active_history_conflicts_fail_closed() {
3570        let history_receipt = rotation(
3571            operation_id(9),
3572            SupervisorRotationPersistencePhase::Completed,
3573            None,
3574            binding(13, "history-previous", 1),
3575            binding(14, "history-next", 2),
3576        );
3577        let history = std::collections::BTreeMap::from([(
3578            history_receipt.operation_id(),
3579            history_receipt.clone(),
3580        )]);
3581        let wrapper = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3582            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3583                15,
3584                "current-supervisor",
3585                3,
3586            ))),
3587            terminal_receipts: history,
3588        });
3589
3590        let mut duplicate = encoded_value(&wrapper);
3591        let receipt = duplicate["supervisor_authority"]["terminal_receipts"][0].clone();
3592        duplicate["supervisor_authority"]["terminal_receipts"]
3593            .as_array_mut()
3594            .expect("history receipt array")
3595            .push(receipt);
3596        assert_decode_fails(duplicate);
3597
3598        let mut nested = encoded_value(&wrapper);
3599        let nested_current = nested["supervisor_authority"].clone();
3600        nested["supervisor_authority"]["current"] = nested_current;
3601        assert_decode_fails(nested);
3602
3603        let active_conflict = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3604            current: Box::new(SupervisorAuthoritySnapshot::RotationOperation(
3605                history_receipt.clone(),
3606            )),
3607            terminal_receipts: std::collections::BTreeMap::from([(
3608                history_receipt.operation_id(),
3609                history_receipt,
3610            )]),
3611        });
3612        assert!(
3613            MachineLifecycleStoreRecord::from_snapshot(&active_conflict)
3614                .encode()
3615                .is_err()
3616        );
3617
3618        let empty_history = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3619            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3620                20,
3621                "current-supervisor",
3622                4,
3623            ))),
3624            terminal_receipts: std::collections::BTreeMap::new(),
3625        });
3626        assert!(
3627            MachineLifecycleStoreRecord::from_snapshot(&empty_history)
3628                .encode()
3629                .is_err()
3630        );
3631        assert!(
3632            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&empty_history))
3633                .is_err()
3634        );
3635
3636        let mismatched_key = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3637            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3638                21,
3639                "current-supervisor",
3640                4,
3641            ))),
3642            terminal_receipts: std::collections::BTreeMap::from([(
3643                operation_id(99),
3644                rotation(
3645                    operation_id(98),
3646                    SupervisorRotationPersistencePhase::Completed,
3647                    None,
3648                    binding(22, "history-previous", 2),
3649                    binding(23, "history-next", 3),
3650                ),
3651            )]),
3652        });
3653        assert!(
3654            MachineLifecycleStoreRecord::from_snapshot(&mismatched_key)
3655                .encode()
3656                .is_err()
3657        );
3658    }
3659
3660    #[test]
3661    fn history_current_epoch_and_same_epoch_identity_must_cohere() {
3662        let previous = binding(38, "history-previous", 12);
3663        let next = binding(39, "history-next", 13);
3664        let completed = rotation(
3665            operation_id(107),
3666            SupervisorRotationPersistencePhase::Completed,
3667            None,
3668            previous.clone(),
3669            next.clone(),
3670        );
3671        let history =
3672            std::collections::BTreeMap::from([(completed.operation_id(), completed.clone())]);
3673
3674        let stale_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3675            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3676                38,
3677                "refreshed-history-previous",
3678                12,
3679            ))),
3680            terminal_receipts: history.clone(),
3681        });
3682        assert!(
3683            MachineLifecycleStoreRecord::from_snapshot(&stale_current)
3684                .encode()
3685                .is_err()
3686        );
3687        assert!(
3688            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(&stale_current))
3689                .is_err()
3690        );
3691
3692        let conflicting_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3693            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3694                40,
3695                "conflicting-current",
3696                13,
3697            ))),
3698            terminal_receipts: history.clone(),
3699        });
3700        assert!(
3701            MachineLifecycleStoreRecord::from_snapshot(&conflicting_current)
3702                .encode()
3703                .is_err()
3704        );
3705        assert!(
3706            decode_machine_lifecycle_store_record(&encode_unvalidated_snapshot(
3707                &conflicting_current,
3708            ))
3709            .is_err()
3710        );
3711
3712        let route_refreshed_current = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3713            current: Box::new(SupervisorAuthoritySnapshot::Bound(binding(
3714                39,
3715                "route-refreshed-history-next",
3716                13,
3717            ))),
3718            terminal_receipts: history,
3719        });
3720        assert_eq!(
3721            decode_machine_lifecycle_store_record(&encode_snapshot(&route_refreshed_current))
3722                .expect("same identity may refresh route metadata within one epoch"),
3723            route_refreshed_current
3724        );
3725    }
3726
3727    #[test]
3728    fn terminal_history_survives_later_rotation_and_recovery() {
3729        let first = rotation(
3730            operation_id(10),
3731            SupervisorRotationPersistencePhase::Completed,
3732            None,
3733            binding(16, "first-supervisor", 1),
3734            binding(17, "second-supervisor", 2),
3735        );
3736        let rejected = rotation(
3737            operation_id(11),
3738            SupervisorRotationPersistencePhase::Rejected,
3739            Some(SupervisorRotationRejection::TargetEpochNotAdvanced),
3740            binding(17, "second-supervisor", 2),
3741            binding(18, "rejected-supervisor", 2),
3742        );
3743        let later = rotation(
3744            operation_id(12),
3745            SupervisorRotationPersistencePhase::Completed,
3746            None,
3747            binding(17, "second-supervisor", 2),
3748            binding(19, "current-supervisor", 3),
3749        );
3750        let snapshot = snapshot(SupervisorAuthoritySnapshot::WithRotationHistory {
3751            current: Box::new(SupervisorAuthoritySnapshot::RotationOperation(later)),
3752            terminal_receipts: std::collections::BTreeMap::from([
3753                (first.operation_id(), first),
3754                (rejected.operation_id(), rejected),
3755            ]),
3756        });
3757
3758        let decoded = decode_machine_lifecycle_store_record(&encode_snapshot(&snapshot))
3759            .expect("later rotation and old terminal history must recover together");
3760        assert_eq!(decoded, snapshot);
3761    }
3762}