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