Skip to main content

meerkat_runtime/store/
memory.rs

1//! InMemoryRuntimeStore — in-memory implementation for testing/ephemeral.
2//!
3//! Uses `tokio::sync::Mutex` per the in-memory concurrency rule.
4//! All mutations complete inside one lock acquisition (no lock held across .await).
5
6use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
7use std::sync::Arc;
8use std::sync::Mutex as StdMutex;
9#[cfg(test)]
10use std::sync::atomic::{AtomicUsize, Ordering};
11
12use indexmap::IndexMap;
13use meerkat_core::lifecycle::{InputId, RunBoundaryReceipt, RunId};
14#[cfg(not(target_arch = "wasm32"))]
15use tokio::sync::Mutex;
16#[cfg(target_arch = "wasm32")]
17use tokio_with_wasm::alias::sync::Mutex;
18
19use super::{
20    AuthOAuthFlowSnapshotUpdate, CommittedWholeBlobProvisionalTail, CommittedWholeBlobSnapshot,
21    ExactInputStateObservation, FencedInputStateBatchCasOutcome, FencedMachineLifecycleCasOutcome,
22    InputStateBatchCasImplementationProfile, InputStateBatchCasOutcome, InputStateRow,
23    MachineLifecycleCasOutcome, MachineLifecycleCommit, MachineLifecycleExpectedVersion,
24    MachineLifecycleObservation, MachineLifecycleStoreRecord, PreparedRecoveryInputSnapshot,
25    PreparedRecoveryInputStateMutation, PreparedRuntimeSessionCommitResult,
26    PreparedWholeBlobProvisionalTail, PreparedWholeBlobRewriteStoreParts,
27    PreparedWholeBlobSnapshot, PreparedWholeBlobSnapshotCas, RecoveryInputSetRevision,
28    RecoveryInputStateMutation, RuntimeDeliveryAuthorityCasOutcome, RuntimeDeliveryAuthorityRecord,
29    RuntimeDeliveryStoreRecord, RuntimeSessionAuthority, RuntimeSessionAuthorityReadCost,
30    RuntimeSessionPersistenceProfile, RuntimeStore, RuntimeStoreError, RuntimeStoreWriteFence,
31    RuntimeStoreWriteFenceOutcome, SerializedSessionSnapshot, WholeBlobProvisionalTailAuthority,
32    WholeBlobSnapshotCasOutcome, WholeBlobStoreAuthority, classify_machine_lifecycle_record,
33    complete_compaction_projection_intent, decoded_prepared_machine_lifecycle_replacement,
34    execute_runtime_store_write_fence, parsed_whole_blob_snapshot, prepare_input_state_batch_cas,
35    prepare_machine_lifecycle_replacement, prepare_recovery_input_state_mutations,
36    validate_input_state_batch_read_ids, validate_machine_lifecycle_replacement,
37};
38use crate::identifiers::{IdempotencyKey, LogicalRuntimeId};
39use crate::input_state::{InputStatePersistenceRecord, StoredInputState};
40use crate::ops_lifecycle::PersistedOpsSnapshot;
41
42/// Receipt key: (runtime_id, run_id, sequence).
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44struct ReceiptKey {
45    runtime_id: String,
46    run_id: RunId,
47    sequence: u64,
48}
49
50#[derive(Debug, Clone)]
51struct CompactionOutboxEntry {
52    intent: meerkat_core::CompactionProjectionIntent,
53    finalized: bool,
54}
55
56#[derive(Debug, Clone)]
57struct StoredWholeBlobProvisionalTail {
58    authority: WholeBlobProvisionalTailAuthority,
59    candidate_bytes: Arc<Vec<u8>>,
60    conversation_digest: String,
61    message_count: u64,
62    catalog_entry: super::RuntimeSessionCatalogEntry,
63    compaction_projection_intents: Vec<meerkat_core::CompactionProjectionIntent>,
64}
65
66#[cfg(test)]
67type InputStateBatchCasTestBlock = (
68    Arc<crate::tokio::sync::Notify>,
69    Arc<crate::tokio::sync::Notify>,
70);
71
72/// Inner state protected by the mutex.
73#[derive(Debug, Default)]
74struct Inner {
75    /// runtime_id → (input_id → StoredInputState). IndexMap for deterministic iteration order.
76    input_states: HashMap<String, IndexMap<InputId, StoredInputState>>,
77    /// Runtime id → canonical owners of unfinished terminal work.
78    pending_terminal_owners: HashMap<String, BTreeSet<uuid::Uuid>>,
79    /// Runtime id → canonical complete set of nonterminal input ids.
80    recovery_nonterminal_inputs: HashMap<String, BTreeSet<uuid::Uuid>>,
81    /// Runtime id → store-owned revision bumped for every input-row mutation.
82    ///
83    /// Missing is canonical generation zero and remains a real absence fence.
84    recovery_input_set_revisions: HashMap<String, u64>,
85    /// Runtime id → exact idempotency key → input id.
86    input_idempotency_index: HashMap<String, HashMap<String, InputId>>,
87    /// Receipt storage.
88    receipts: HashMap<ReceiptKey, RunBoundaryReceipt>,
89    /// Exact machine-authorized recovery witness by runtime/candidate.
90    recovery_boundaries: HashMap<(String, String), super::CommittedRecoveryBoundary>,
91    /// Runtime session snapshots keyed by canonical runtime id.
92    sessions: HashMap<String, Arc<Vec<u8>>>,
93    /// Fixed-size authority paired atomically with every WholeBlob snapshot.
94    session_authorities: HashMap<String, WholeBlobStoreAuthority>,
95    /// Body-free session listing/lifecycle projection.
96    session_catalog: HashMap<String, super::RuntimeSessionCatalogEntry>,
97    /// Store-owned provisional candidate body and exact base/run identity.
98    whole_blob_provisional_tails: HashMap<String, StoredWholeBlobProvisionalTail>,
99    /// Canonical runtime ids whose projection fallback is quarantined.
100    ///
101    /// Mirrors the durable SQLite `runtime_projection_quarantine` table: set
102    /// when a rejected runtime snapshot is cleared via
103    /// `clear_session_snapshot_if_current`, cleared whenever a live snapshot is
104    /// written for the runtime.
105    projection_quarantine: HashSet<String>,
106    /// Exact persisted machine-lifecycle bytes. Raw storage is required so
107    /// malformed and unsupported rows remain observable instead of being
108    /// normalized by an eager typed decode.
109    runtime_lifecycle: HashMap<String, Vec<u8>>,
110    /// Persisted ops lifecycle snapshots.
111    ops_lifecycle_snapshots: HashMap<String, PersistedOpsSnapshot>,
112    /// Exact ops epochs retired by atomic unregister finalization. Tombstones
113    /// outlive row deletion so detached callbacks cannot resurrect them.
114    retired_ops_epochs: HashSet<(String, meerkat_core::RuntimeEpochId)>,
115    /// Runtime id -> transcript-rewrite-keyed compaction projection outbox.
116    compaction_projection_outbox:
117        HashMap<String, HashMap<meerkat_core::CompactionProjectionId, CompactionOutboxEntry>>,
118    /// Exact generated runtime-delivery authority by logical runtime.
119    runtime_delivery_authority: HashMap<String, RuntimeDeliveryAuthorityRecord>,
120    /// Durable runtime-delivery rows ordered by generated sequence.
121    runtime_delivery_records: HashMap<String, BTreeMap<u64, RuntimeDeliveryStoreRecord>>,
122}
123
124fn sync_runtime_session_catalog_lifecycle(
125    inner: &mut Inner,
126    runtime_id: &str,
127    runtime_state: crate::RuntimeState,
128) {
129    if let Some(entry) = inner.session_catalog.get_mut(runtime_id) {
130        entry.set_runtime_state(Some(runtime_state));
131    }
132}
133
134fn store_input_state_prechecked(
135    inner: &mut Inner,
136    runtime_id: &str,
137    bundle: StoredInputState,
138    next_revision: u64,
139) {
140    let input_id = bundle.state.input_id.clone();
141    let new_idempotency_key = bundle
142        .state
143        .idempotency_key
144        .as_ref()
145        .map(|key| key.0.clone());
146    let old_idempotency_key = inner
147        .input_states
148        .get(runtime_id)
149        .and_then(|states| states.get(&input_id))
150        .and_then(|state| state.state.idempotency_key.as_ref())
151        .map(|key| key.0.clone());
152    let owners_empty = {
153        let owners = inner
154            .pending_terminal_owners
155            .entry(runtime_id.to_string())
156            .or_default();
157        if super::input_state_is_pending_terminal_owner(&bundle.state) {
158            owners.insert(input_id.0);
159        } else {
160            owners.remove(&input_id.0);
161        }
162        owners.is_empty()
163    };
164    if owners_empty {
165        inner.pending_terminal_owners.remove(runtime_id);
166    }
167    let nonterminal_empty = {
168        let nonterminal = inner
169            .recovery_nonterminal_inputs
170            .entry(runtime_id.to_string())
171            .or_default();
172        if super::input_state_is_recovery_nonterminal(&bundle) {
173            nonterminal.insert(input_id.0);
174        } else {
175            nonterminal.remove(&input_id.0);
176        }
177        nonterminal.is_empty()
178    };
179    if nonterminal_empty {
180        inner.recovery_nonterminal_inputs.remove(runtime_id);
181    }
182    inner
183        .input_states
184        .entry(runtime_id.to_string())
185        .or_default()
186        .insert(input_id.clone(), bundle);
187    if old_idempotency_key != new_idempotency_key {
188        let remove_empty_index = if let Some(old_key) = old_idempotency_key
189            && let Some(index) = inner.input_idempotency_index.get_mut(runtime_id)
190        {
191            if index.get(&old_key) == Some(&input_id) {
192                index.remove(&old_key);
193            }
194            index.is_empty()
195        } else {
196            false
197        };
198        if remove_empty_index {
199            inner.input_idempotency_index.remove(runtime_id);
200        }
201        if let Some(new_key) = new_idempotency_key {
202            inner
203                .input_idempotency_index
204                .entry(runtime_id.to_string())
205                .or_default()
206                .insert(new_key, input_id);
207        }
208    }
209    inner
210        .recovery_input_set_revisions
211        .insert(runtime_id.to_string(), next_revision);
212}
213
214fn store_input_state(
215    inner: &mut Inner,
216    runtime_id: &str,
217    bundle: StoredInputState,
218) -> Result<(), RuntimeStoreError> {
219    let next_revision = inner
220        .recovery_input_set_revisions
221        .get(runtime_id)
222        .copied()
223        .unwrap_or(0)
224        .checked_add(1)
225        .ok_or_else(|| {
226            RuntimeStoreError::WriteFailed(format!(
227                "input-set revision exhausted for runtime {runtime_id}"
228            ))
229        })?;
230    let input_id = &bundle.state.input_id;
231    if let Some(key) = bundle.state.idempotency_key.as_ref()
232        && inner
233            .input_idempotency_index
234            .get(runtime_id)
235            .and_then(|index| index.get(&key.0))
236            .is_some_and(|indexed_input_id| indexed_input_id != input_id)
237    {
238        return Err(RuntimeStoreError::WriteFailed(format!(
239            "idempotency key `{key}` already belongs to another input in runtime {runtime_id}"
240        )));
241    }
242    store_input_state_prechecked(inner, runtime_id, bundle, next_revision);
243    Ok(())
244}
245
246fn delete_input_state_prechecked(
247    inner: &mut Inner,
248    runtime_id: &str,
249    input_id: &InputId,
250    next_revision: u64,
251) {
252    let removed = inner
253        .input_states
254        .get_mut(runtime_id)
255        .and_then(|states| states.shift_remove(input_id));
256    let Some(removed) = removed else {
257        // Prepared batches prove row presence while holding the same store
258        // mutex. Treat an impossible internal mismatch as a no-op instead of
259        // introducing a fallible boundary after sibling effects are visible.
260        return;
261    };
262    if inner
263        .input_states
264        .get(runtime_id)
265        .is_some_and(IndexMap::is_empty)
266    {
267        inner.input_states.remove(runtime_id);
268    }
269    if let Some(owners) = inner.pending_terminal_owners.get_mut(runtime_id) {
270        owners.remove(&input_id.0);
271        if owners.is_empty() {
272            inner.pending_terminal_owners.remove(runtime_id);
273        }
274    }
275    if let Some(nonterminal) = inner.recovery_nonterminal_inputs.get_mut(runtime_id) {
276        nonterminal.remove(&input_id.0);
277        if nonterminal.is_empty() {
278            inner.recovery_nonterminal_inputs.remove(runtime_id);
279        }
280    }
281    let remove_empty_index = if let Some(key) = removed.state.idempotency_key.as_ref()
282        && let Some(index) = inner.input_idempotency_index.get_mut(runtime_id)
283    {
284        if index.get(&key.0) == Some(input_id) {
285            index.remove(&key.0);
286        }
287        index.is_empty()
288    } else {
289        false
290    };
291    if remove_empty_index {
292        inner.input_idempotency_index.remove(runtime_id);
293    }
294    inner
295        .recovery_input_set_revisions
296        .insert(runtime_id.to_string(), next_revision);
297}
298
299enum MemoryInputStateMutation {
300    Upsert(StoredInputState),
301    Delete(InputId),
302}
303
304impl MemoryInputStateMutation {
305    fn input_id(&self) -> &InputId {
306        match self {
307            Self::Upsert(bundle) => &bundle.state.input_id,
308            Self::Delete(input_id) => input_id,
309        }
310    }
311
312    fn target_idempotency_key(&self) -> Option<&str> {
313        match self {
314            Self::Upsert(bundle) => bundle
315                .state
316                .idempotency_key
317                .as_ref()
318                .map(|key| key.0.as_str()),
319            Self::Delete(_) => None,
320        }
321    }
322}
323
324struct PreparedMemoryInputStateMutation {
325    mutation: MemoryInputStateMutation,
326    next_revision: u64,
327}
328
329/// Validate a complete in-memory input mutation set before any sibling effect
330/// becomes visible.
331///
332/// Besides revision exhaustion and duplicate input ids, this releases and
333/// reclaims idempotency keys as one logical batch. That permits valid key swaps
334/// while rejecting a collision with any input outside the mutation set.
335fn prepare_memory_input_state_mutations(
336    inner: &Inner,
337    runtime_id: &str,
338    mutations: Vec<MemoryInputStateMutation>,
339) -> Result<Vec<PreparedMemoryInputStateMutation>, RuntimeStoreError> {
340    let mut target_ids = HashSet::with_capacity(mutations.len());
341    for mutation in &mutations {
342        if !target_ids.insert(mutation.input_id().clone()) {
343            return Err(RuntimeStoreError::WriteFailed(format!(
344                "atomic input-state mutation set repeats input {} in runtime {runtime_id}",
345                mutation.input_id()
346            )));
347        }
348        if matches!(mutation, MemoryInputStateMutation::Delete(_))
349            && !inner
350                .input_states
351                .get(runtime_id)
352                .is_some_and(|states| states.contains_key(mutation.input_id()))
353        {
354            return Err(RuntimeStoreError::InputRowVersionConflict {
355                input_id: mutation.input_id().to_string(),
356            });
357        }
358    }
359
360    let mut target_keys: HashMap<String, InputId> = HashMap::new();
361    for mutation in &mutations {
362        let Some(key) = mutation.target_idempotency_key() else {
363            continue;
364        };
365        if let Some(other_input_id) =
366            target_keys.insert(key.to_string(), mutation.input_id().clone())
367            && other_input_id != *mutation.input_id()
368        {
369            return Err(RuntimeStoreError::WriteFailed(format!(
370                "idempotency key `{key}` is claimed by both {other_input_id} and {} in runtime {runtime_id}",
371                mutation.input_id()
372            )));
373        }
374        if let Some(existing_input_id) = inner
375            .input_idempotency_index
376            .get(runtime_id)
377            .and_then(|index| index.get(key))
378            && existing_input_id != mutation.input_id()
379            && !target_ids.contains(existing_input_id)
380        {
381            return Err(RuntimeStoreError::WriteFailed(format!(
382                "idempotency key `{key}` already belongs to input {existing_input_id} outside the atomic mutation set for runtime {runtime_id}"
383            )));
384        }
385    }
386
387    let current_revision = inner
388        .recovery_input_set_revisions
389        .get(runtime_id)
390        .copied()
391        .unwrap_or(0);
392    let mutation_count = u64::try_from(mutations.len()).map_err(|_| {
393        RuntimeStoreError::WriteFailed(format!(
394            "input-set mutation count does not fit the revision for runtime {runtime_id}"
395        ))
396    })?;
397    current_revision
398        .checked_add(mutation_count)
399        .ok_or_else(|| {
400            RuntimeStoreError::WriteFailed(format!(
401                "input-set revision exhausted for runtime {runtime_id}"
402            ))
403        })?;
404
405    mutations
406        .into_iter()
407        .enumerate()
408        .map(|(index, mutation)| {
409            let offset = u64::try_from(index + 1).map_err(|_| {
410                RuntimeStoreError::WriteFailed(format!(
411                    "input-set mutation ordinal does not fit the revision for runtime {runtime_id}"
412                ))
413            })?;
414            Ok(PreparedMemoryInputStateMutation {
415                mutation,
416                next_revision: current_revision + offset,
417            })
418        })
419        .collect()
420}
421
422/// Apply a previously validated input batch while retaining the same store
423/// mutex. No fallible work remains here, so a larger boundary can safely write
424/// its other effects before or after this call.
425fn apply_prepared_memory_input_state_mutations(
426    inner: &mut Inner,
427    runtime_id: &str,
428    prepared: Vec<PreparedMemoryInputStateMutation>,
429) {
430    let remove_empty_index = if let Some(index) = inner.input_idempotency_index.get_mut(runtime_id)
431    {
432        for mutation in &prepared {
433            let input_id = mutation.mutation.input_id();
434            let old_key = inner
435                .input_states
436                .get(runtime_id)
437                .and_then(|states| states.get(input_id))
438                .and_then(|bundle| bundle.state.idempotency_key.as_ref())
439                .map(|key| key.0.as_str());
440            if let Some(old_key) = old_key
441                && Some(old_key) != mutation.mutation.target_idempotency_key()
442                && index.get(old_key) == Some(input_id)
443            {
444                index.remove(old_key);
445            }
446        }
447        index.is_empty()
448    } else {
449        false
450    };
451    if remove_empty_index {
452        inner.input_idempotency_index.remove(runtime_id);
453    }
454
455    for prepared in prepared {
456        match prepared.mutation {
457            MemoryInputStateMutation::Upsert(bundle) => {
458                store_input_state_prechecked(inner, runtime_id, bundle, prepared.next_revision);
459            }
460            MemoryInputStateMutation::Delete(input_id) => {
461                delete_input_state_prechecked(inner, runtime_id, &input_id, prepared.next_revision);
462            }
463        }
464    }
465}
466
467/// In-memory runtime store. Thread-safe via `tokio::sync::Mutex`.
468#[derive(Debug, Clone)]
469pub struct InMemoryRuntimeStore {
470    inner: Arc<Mutex<Inner>>,
471    auth_oauth_flow_snapshot: Arc<StdMutex<Option<Vec<u8>>>>,
472    #[cfg(test)]
473    input_state_batch_cas_before: Arc<StdMutex<Option<InputStateBatchCasTestBlock>>>,
474    #[cfg(test)]
475    input_state_batch_cas_after_commit: Arc<StdMutex<Option<InputStateBatchCasTestBlock>>>,
476    #[cfg(test)]
477    machine_lifecycle_cas_conflicts_remaining: Arc<AtomicUsize>,
478    #[cfg(test)]
479    machine_lifecycle_observe_errors_remaining: Arc<AtomicUsize>,
480    /// Candidate bytes shipped into the snapshot byte-equality compare.
481    /// Observability seam for the length-gate regression tests only.
482    #[cfg(test)]
483    snapshot_byte_probe_bytes: Arc<std::sync::atomic::AtomicU64>,
484}
485
486impl InMemoryRuntimeStore {
487    pub fn new() -> Self {
488        Self {
489            inner: Arc::new(Mutex::new(Inner::default())),
490            auth_oauth_flow_snapshot: Arc::new(StdMutex::new(None)),
491            #[cfg(test)]
492            input_state_batch_cas_before: Arc::new(StdMutex::new(None)),
493            #[cfg(test)]
494            input_state_batch_cas_after_commit: Arc::new(StdMutex::new(None)),
495            #[cfg(test)]
496            machine_lifecycle_cas_conflicts_remaining: Arc::new(AtomicUsize::new(0)),
497            #[cfg(test)]
498            machine_lifecycle_observe_errors_remaining: Arc::new(AtomicUsize::new(0)),
499            #[cfg(test)]
500            snapshot_byte_probe_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
501        }
502    }
503
504    /// Total candidate bytes this store has shipped into the snapshot
505    /// byte-equality compare. Length-gate regression tests only.
506    #[cfg(test)]
507    pub(crate) fn snapshot_byte_probe_bytes(&self) -> u64 {
508        self.snapshot_byte_probe_bytes
509            .load(std::sync::atomic::Ordering::Relaxed)
510    }
511
512    #[cfg(test)]
513    pub(crate) fn block_next_input_state_batch_cas_before_mutation(
514        &self,
515        entered: Arc<crate::tokio::sync::Notify>,
516        release: Arc<crate::tokio::sync::Notify>,
517    ) {
518        *self
519            .input_state_batch_cas_before
520            .lock()
521            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some((entered, release));
522    }
523
524    #[cfg(test)]
525    pub(crate) fn block_next_input_state_batch_cas_after_commit(
526        &self,
527        entered: Arc<crate::tokio::sync::Notify>,
528        release: Arc<crate::tokio::sync::Notify>,
529    ) {
530        *self
531            .input_state_batch_cas_after_commit
532            .lock()
533            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some((entered, release));
534    }
535
536    #[cfg(test)]
537    pub(crate) async fn seed_machine_lifecycle_raw(
538        &self,
539        runtime_id: &LogicalRuntimeId,
540        bytes: Vec<u8>,
541    ) {
542        self.inner
543            .lock()
544            .await
545            .runtime_lifecycle
546            .insert(runtime_id.0.clone(), bytes);
547    }
548
549    #[cfg(test)]
550    pub(crate) fn conflict_next_machine_lifecycle_cas(&self) {
551        self.machine_lifecycle_cas_conflicts_remaining
552            .fetch_add(1, Ordering::SeqCst);
553    }
554
555    #[cfg(test)]
556    pub(crate) fn fail_next_machine_lifecycle_observation(&self) {
557        self.machine_lifecycle_observe_errors_remaining
558            .fetch_add(1, Ordering::SeqCst);
559    }
560
561    async fn commit_session_snapshot_inner(
562        &self,
563        runtime_id: &LogicalRuntimeId,
564        prepared: PreparedWholeBlobSnapshot,
565    ) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
566        if &LogicalRuntimeId::for_session(prepared.session().id()) != runtime_id {
567            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
568                runtime_id: runtime_id.to_string(),
569                detail: format!(
570                    "WholeBlob payload session {} does not bind this runtime",
571                    prepared.session().id()
572                ),
573            });
574        }
575        let mut inner = self.inner.lock().await;
576        ensure_compaction_intents_already_outboxed(&inner, runtime_id, prepared.session())?;
577        commit_prepared_whole_blob_snapshot_locked(&mut inner, runtime_id, prepared)
578    }
579
580    async fn atomic_apply_prepared_whole_blob(
581        &self,
582        runtime_id: &LogicalRuntimeId,
583        prepared_session: Option<PreparedWholeBlobSnapshot>,
584        receipt: RunBoundaryReceipt,
585        input_updates: Vec<InputStatePersistenceRecord>,
586        session_store_key: Option<meerkat_core::types::SessionId>,
587    ) -> Result<Option<WholeBlobStoreAuthority>, RuntimeStoreError> {
588        let rid = runtime_id.0.clone();
589        let compaction_intents = prepared_session
590            .as_ref()
591            .map(|prepared| super::validated_compaction_projection_intents(prepared.session()))
592            .transpose()?
593            .unwrap_or_default();
594        if let (Some(prepared), Some(session_store_key)) =
595            (prepared_session.as_ref(), session_store_key.as_ref())
596            && prepared.session().id() != session_store_key
597        {
598            return Err(RuntimeStoreError::SessionKeyMismatch {
599                expected: session_store_key.clone(),
600                actual: prepared.session().id().clone(),
601            });
602        }
603
604        let input_updates = input_updates
605            .into_iter()
606            .map(InputStatePersistenceRecord::into_stored_and_expected)
607            .collect::<Vec<_>>();
608        let key = ReceiptKey {
609            runtime_id: rid.clone(),
610            run_id: receipt.run_id.clone(),
611            sequence: receipt.sequence,
612        };
613        let mut inner = self.inner.lock().await;
614        if prepared_session.is_none() && inner.whole_blob_provisional_tails.contains_key(&rid) {
615            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
616                runtime_id: rid,
617                detail: "receipt-only boundary cannot bypass a store-owned WholeBlob candidate"
618                    .to_string(),
619            });
620        }
621        if let Some(existing) = inner.compaction_projection_outbox.get(&rid) {
622            for intent in &compaction_intents {
623                if let Some(entry) = existing.get(&intent.projection) {
624                    if entry.finalized {
625                        return Err(RuntimeStoreError::WriteFailed(format!(
626                            "atomic session snapshot replays finalized compaction intent {}",
627                            intent.projection.revision()
628                        )));
629                    }
630                    if entry.intent != *intent {
631                        return Err(RuntimeStoreError::WriteFailed(format!(
632                            "conflicting compaction outbox intent for rewrite {}",
633                            intent.projection.revision()
634                        )));
635                    }
636                }
637            }
638        }
639        if inner.receipts.contains_key(&key) {
640            return Err(RuntimeStoreError::WriteFailed(format!(
641                "boundary receipt already exists for runtime '{}' run {} sequence {}",
642                runtime_id, receipt.run_id, receipt.sequence
643            )));
644        }
645        precheck_fenced_input_updates(inner.input_states.get(&rid), &input_updates)?;
646        let prepared_input_mutations = prepare_memory_input_state_mutations(
647            &inner,
648            &rid,
649            input_updates
650                .into_iter()
651                .map(|(bundle, _expected)| MemoryInputStateMutation::Upsert(bundle))
652                .collect(),
653        )?;
654
655        // The body/authority promotion is the final fallible step before any
656        // other mutation. Everything below is infallible under this lock, so a
657        // stale base/run/candidate cannot leave a partial receipt or outbox.
658        let authority = prepared_session
659            .map(|prepared| {
660                commit_prepared_whole_blob_snapshot_locked(&mut inner, runtime_id, prepared)
661            })
662            .transpose()?;
663        let outbox = inner
664            .compaction_projection_outbox
665            .entry(rid.clone())
666            .or_default();
667        for intent in compaction_intents {
668            outbox
669                .entry(intent.projection.clone())
670                .or_insert(CompactionOutboxEntry {
671                    intent,
672                    finalized: false,
673                });
674        }
675        inner.receipts.insert(key, receipt);
676        apply_prepared_memory_input_state_mutations(&mut inner, &rid, prepared_input_mutations);
677        Ok(authority)
678    }
679
680    async fn atomic_apply_prepared_whole_blob_with_machine_lifecycle(
681        &self,
682        runtime_id: &LogicalRuntimeId,
683        prepared: PreparedWholeBlobSnapshot,
684        receipt: RunBoundaryReceipt,
685        machine_lifecycle: MachineLifecycleCommit,
686        input_updates: Vec<InputStatePersistenceRecord>,
687        session_store_key: meerkat_core::types::SessionId,
688    ) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
689        if prepared.session().id() != &session_store_key {
690            return Err(RuntimeStoreError::SessionKeyMismatch {
691                expected: session_store_key,
692                actual: prepared.session().id().clone(),
693            });
694        }
695        let rid = runtime_id.0.clone();
696        let compaction_intents =
697            super::validated_compaction_projection_intents(prepared.session())?;
698        let machine_lifecycle_record = machine_lifecycle.store_record().encode()?;
699        let lifecycle_expected = machine_lifecycle.expected_version().cloned();
700        let input_updates = input_updates
701            .into_iter()
702            .map(InputStatePersistenceRecord::into_stored_and_expected)
703            .collect::<Vec<_>>();
704        let key = ReceiptKey {
705            runtime_id: rid.clone(),
706            run_id: receipt.run_id.clone(),
707            sequence: receipt.sequence,
708        };
709
710        let mut inner = self.inner.lock().await;
711        if let Some(existing) = inner.compaction_projection_outbox.get(&rid) {
712            for intent in &compaction_intents {
713                if let Some(entry) = existing.get(&intent.projection) {
714                    if entry.finalized {
715                        return Err(RuntimeStoreError::WriteFailed(format!(
716                            "atomic session snapshot replays finalized compaction intent {}",
717                            intent.projection.revision()
718                        )));
719                    }
720                    if entry.intent != *intent {
721                        return Err(RuntimeStoreError::WriteFailed(format!(
722                            "conflicting compaction outbox intent for rewrite {}",
723                            intent.projection.revision()
724                        )));
725                    }
726                }
727            }
728        }
729        if inner.receipts.contains_key(&key) {
730            return Err(RuntimeStoreError::WriteFailed(format!(
731                "boundary receipt already exists for runtime '{}' run {} sequence {}",
732                runtime_id, receipt.run_id, receipt.sequence
733            )));
734        }
735        precheck_fenced_input_updates(inner.input_states.get(&rid), &input_updates)?;
736        let prepared_input_mutations = prepare_memory_input_state_mutations(
737            &inner,
738            &rid,
739            input_updates
740                .into_iter()
741                .map(|(bundle, _expected)| MemoryInputStateMutation::Upsert(bundle))
742                .collect(),
743        )?;
744        if let Some(expected) = &lifecycle_expected {
745            let existing = inner.runtime_lifecycle.get(&rid);
746            let matches = match expected {
747                MachineLifecycleExpectedVersion::Missing => existing.is_none(),
748                MachineLifecycleExpectedVersion::Version(version) => {
749                    existing.is_some_and(|bytes| {
750                        super::MachineLifecycleObservationVersion::from_raw_record(bytes)
751                            == *version
752                    })
753                }
754            };
755            if !matches {
756                return Err(RuntimeStoreError::MachineLifecycleVersionConflict { runtime_id: rid });
757            }
758        }
759
760        let authority =
761            commit_prepared_whole_blob_snapshot_locked(&mut inner, runtime_id, prepared)?;
762        let outbox = inner
763            .compaction_projection_outbox
764            .entry(rid.clone())
765            .or_default();
766        for intent in compaction_intents {
767            outbox
768                .entry(intent.projection.clone())
769                .or_insert(CompactionOutboxEntry {
770                    intent,
771                    finalized: false,
772                });
773        }
774        inner
775            .runtime_lifecycle
776            .insert(rid.clone(), machine_lifecycle_record);
777        inner.receipts.insert(key, receipt);
778        apply_prepared_memory_input_state_mutations(&mut inner, &rid, prepared_input_mutations);
779        Ok(authority)
780    }
781
782    async fn atomic_promote_whole_blob(
783        &self,
784        runtime_id: &LogicalRuntimeId,
785        promotion: super::PreparedWholeBlobProvisionalPromotion,
786        receipt: RunBoundaryReceipt,
787        input_updates: Vec<InputStatePersistenceRecord>,
788        session_store_key: meerkat_core::types::SessionId,
789    ) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
790        let (authority, checkpoint_conversation_digest, checkpoint_message_count) =
791            promotion.into_parts();
792        if authority.session_id() != &session_store_key {
793            return Err(RuntimeStoreError::SessionKeyMismatch {
794                expected: authority.session_id().clone(),
795                actual: session_store_key,
796            });
797        }
798        let rid = runtime_id.0.clone();
799        let key = ReceiptKey {
800            runtime_id: rid.clone(),
801            run_id: receipt.run_id.clone(),
802            sequence: receipt.sequence,
803        };
804        let input_updates = input_updates
805            .into_iter()
806            .map(InputStatePersistenceRecord::into_stored_and_expected)
807            .collect::<Vec<_>>();
808        let mut inner = self.inner.lock().await;
809        if inner.receipts.contains_key(&key) {
810            return Err(RuntimeStoreError::WriteFailed(format!(
811                "boundary receipt already exists for runtime '{}' run {} sequence {}",
812                runtime_id, receipt.run_id, receipt.sequence
813            )));
814        }
815        precheck_fenced_input_updates(inner.input_states.get(&rid), &input_updates)?;
816        let prepared_input_mutations = prepare_memory_input_state_mutations(
817            &inner,
818            &rid,
819            input_updates
820                .into_iter()
821                .map(|(bundle, _expected)| MemoryInputStateMutation::Upsert(bundle))
822                .collect(),
823        )?;
824        let committed = promote_whole_blob_provisional_locked(
825            &mut inner,
826            runtime_id,
827            &authority,
828            &receipt,
829            &checkpoint_conversation_digest,
830            checkpoint_message_count,
831            None,
832        )?;
833        inner.receipts.insert(key, receipt);
834        apply_prepared_memory_input_state_mutations(&mut inner, &rid, prepared_input_mutations);
835        Ok(committed)
836    }
837
838    async fn atomic_promote_whole_blob_with_machine_lifecycle(
839        &self,
840        runtime_id: &LogicalRuntimeId,
841        promotion: super::PreparedWholeBlobProvisionalPromotion,
842        receipt: RunBoundaryReceipt,
843        machine_lifecycle: MachineLifecycleCommit,
844        input_updates: Vec<InputStatePersistenceRecord>,
845        session_store_key: meerkat_core::types::SessionId,
846    ) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
847        let (authority, checkpoint_conversation_digest, checkpoint_message_count) =
848            promotion.into_parts();
849        if authority.session_id() != &session_store_key {
850            return Err(RuntimeStoreError::SessionKeyMismatch {
851                expected: authority.session_id().clone(),
852                actual: session_store_key,
853            });
854        }
855        let rid = runtime_id.0.clone();
856        let key = ReceiptKey {
857            runtime_id: rid.clone(),
858            run_id: receipt.run_id.clone(),
859            sequence: receipt.sequence,
860        };
861        let machine_lifecycle_record = machine_lifecycle.store_record().encode()?;
862        let lifecycle_expected = machine_lifecycle.expected_version().cloned();
863        let runtime_state = machine_lifecycle.runtime_state();
864        let input_updates = input_updates
865            .into_iter()
866            .map(InputStatePersistenceRecord::into_stored_and_expected)
867            .collect::<Vec<_>>();
868        let mut inner = self.inner.lock().await;
869        if inner.receipts.contains_key(&key) {
870            return Err(RuntimeStoreError::WriteFailed(format!(
871                "boundary receipt already exists for runtime '{}' run {} sequence {}",
872                runtime_id, receipt.run_id, receipt.sequence
873            )));
874        }
875        precheck_fenced_input_updates(inner.input_states.get(&rid), &input_updates)?;
876        let prepared_input_mutations = prepare_memory_input_state_mutations(
877            &inner,
878            &rid,
879            input_updates
880                .into_iter()
881                .map(|(bundle, _expected)| MemoryInputStateMutation::Upsert(bundle))
882                .collect(),
883        )?;
884        if let Some(expected) = &lifecycle_expected {
885            let existing = inner.runtime_lifecycle.get(&rid);
886            let matches = match expected {
887                MachineLifecycleExpectedVersion::Missing => existing.is_none(),
888                MachineLifecycleExpectedVersion::Version(version) => {
889                    existing.is_some_and(|bytes| {
890                        super::MachineLifecycleObservationVersion::from_raw_record(bytes)
891                            == *version
892                    })
893                }
894            };
895            if !matches {
896                return Err(RuntimeStoreError::MachineLifecycleVersionConflict { runtime_id: rid });
897            }
898        }
899        let committed = promote_whole_blob_provisional_locked(
900            &mut inner,
901            runtime_id,
902            &authority,
903            &receipt,
904            &checkpoint_conversation_digest,
905            checkpoint_message_count,
906            Some(runtime_state),
907        )?;
908        inner
909            .runtime_lifecycle
910            .insert(rid.clone(), machine_lifecycle_record);
911        inner.receipts.insert(key, receipt);
912        apply_prepared_memory_input_state_mutations(&mut inner, &rid, prepared_input_mutations);
913        Ok(committed)
914    }
915
916    // RuntimeStore's sealed recovery verb intentionally carries each fenced
917    // boundary component as a separate typed argument.
918    #[allow(clippy::too_many_arguments)]
919    async fn atomic_recover_whole_blob(
920        &self,
921        runtime_id: &LogicalRuntimeId,
922        promotion: super::PreparedWholeBlobRecoveryPromotion,
923        evidence: super::PreparedRecoveryEvidence,
924        receipt: RunBoundaryReceipt,
925        machine_lifecycle: MachineLifecycleCommit,
926        input_updates: Vec<InputStatePersistenceRecord>,
927        session_store_key: meerkat_core::types::SessionId,
928    ) -> Result<PreparedRuntimeSessionCommitResult, RuntimeStoreError> {
929        let (expected, repaired_snapshot) = promotion.into_parts();
930        if expected.session_id() != &session_store_key {
931            return Err(RuntimeStoreError::SessionKeyMismatch {
932                expected: expected.session_id().clone(),
933                actual: session_store_key,
934            });
935        }
936        evidence.verify_input_updates(&input_updates)?;
937        evidence.verify_request_effects(&receipt, &machine_lifecycle)?;
938        let recovery = super::CommittedRecoveryBoundary::from_prepared(&evidence, &receipt);
939        let receipt_key = ReceiptKey {
940            runtime_id: runtime_id.0.clone(),
941            run_id: receipt.run_id.clone(),
942            sequence: receipt.sequence,
943        };
944        let lifecycle_target = machine_lifecycle.store_record().encode()?;
945        let lifecycle_expected =
946            machine_lifecycle
947                .expected_version()
948                .cloned()
949                .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
950                    runtime_id: runtime_id.to_string(),
951                    detail: "WholeBlob recovery lifecycle has no exact predecessor fence"
952                        .to_string(),
953                })?;
954        let runtime_state = machine_lifecycle.runtime_state();
955        let input_updates = input_updates
956            .into_iter()
957            .map(InputStatePersistenceRecord::into_stored_and_expected)
958            .collect::<Vec<_>>();
959        let mut inner = self.inner.lock().await;
960
961        let recovery_key = (runtime_id.0.clone(), evidence.candidate_id().to_string());
962        if let Some(stored) = inner.recovery_boundaries.get(&recovery_key) {
963            if stored != &recovery {
964                return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
965                    runtime_id: runtime_id.to_string(),
966                    detail: "a divergent recovery boundary already exists for this candidate"
967                        .to_string(),
968                });
969            }
970            let (_, _, _, _, recovered_blob_sha256) = evidence
971                .whole_blob_authority_transition()
972                .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
973                    runtime_id: runtime_id.to_string(),
974                    detail: "committed WholeBlob recovery lost its authority transition"
975                        .to_string(),
976                })?;
977            let expected_current = WholeBlobStoreAuthority::issued(
978                evidence.session_id().clone(),
979                expected
980                    .base_store_revision()
981                    .checked_add(1)
982                    .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
983                        runtime_id: runtime_id.to_string(),
984                        detail: "committed WholeBlob recovery revision overflow".to_string(),
985                    })?,
986                recovered_blob_sha256.to_string(),
987            )?;
988            if inner.session_authorities.get(&runtime_id.0) != Some(&expected_current)
989                || inner
990                    .whole_blob_provisional_tails
991                    .contains_key(&runtime_id.0)
992                || inner.runtime_lifecycle.get(&runtime_id.0) != Some(&lifecycle_target)
993                || inner.receipts.get(&receipt_key) != Some(&receipt)
994            {
995                return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
996                    runtime_id: runtime_id.to_string(),
997                    detail: "committed WholeBlob recovery effects were superseded".to_string(),
998                });
999            }
1000            for (target, _) in &input_updates {
1001                let current = inner
1002                    .input_states
1003                    .get(&runtime_id.0)
1004                    .and_then(|states| states.get(&target.state.input_id));
1005                let current_digest = current.map(memory_input_row_version_digest).transpose()?;
1006                let target_digest = memory_input_row_version_digest(target)?;
1007                if current_digest.as_deref() != Some(target_digest.as_str()) {
1008                    return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1009                        runtime_id: runtime_id.to_string(),
1010                        detail: format!(
1011                            "committed recovery input {} was superseded",
1012                            target.state.input_id
1013                        ),
1014                    });
1015                }
1016            }
1017            for enrichment in evidence.receipt_digest_enrichments() {
1018                let enriched = enrichment.enriched_receipt();
1019                let key = ReceiptKey {
1020                    runtime_id: runtime_id.0.clone(),
1021                    run_id: enriched.run_id.clone(),
1022                    sequence: enriched.sequence,
1023                };
1024                if inner.receipts.get(&key) != Some(&enriched) {
1025                    return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1026                        runtime_id: runtime_id.to_string(),
1027                        detail: format!(
1028                            "committed recovery receipt enrichment {}:{} was superseded",
1029                            enriched.run_id, enriched.sequence
1030                        ),
1031                    });
1032                }
1033            }
1034            return Ok(PreparedRuntimeSessionCommitResult::recovery(
1035                super::RuntimeSessionAuthority::WholeBlob(expected_current),
1036                super::RecoveryCommitStatus::AlreadyCommittedExact,
1037            ));
1038        }
1039
1040        let input_snapshot = prepared_memory_recovery_input_snapshot(&inner, runtime_id)?;
1041        if input_snapshot.input_set_revision()
1042            != evidence.predecessor_nonterminal_input_set_revision()
1043            || input_snapshot.exact_set_token()
1044                != evidence.predecessor_nonterminal_input_set_token()
1045        {
1046            return Err(RuntimeStoreError::RecoveryInputSetConflict {
1047                runtime_id: runtime_id.to_string(),
1048            });
1049        }
1050        let current = inner
1051            .session_authorities
1052            .get(&runtime_id.0)
1053            .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
1054                runtime_id: runtime_id.to_string(),
1055                detail: "WholeBlob recovery has no committed base authority".to_string(),
1056            })?;
1057        let stored = inner
1058            .whole_blob_provisional_tails
1059            .get(&runtime_id.0)
1060            .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
1061                runtime_id: runtime_id.to_string(),
1062                detail: "WholeBlob recovery has no provisional candidate".to_string(),
1063            })?;
1064        if current.session_id() != expected.session_id()
1065            || current.store_revision() != expected.base_store_revision()
1066            || current.blob_sha256() != expected.base_blob_sha256()
1067            || stored.authority != expected
1068            || expected.run_id() != &receipt.run_id
1069        {
1070            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1071                runtime_id: runtime_id.to_string(),
1072                detail: "WholeBlob recovery does not exactly match stored base/run/candidate"
1073                    .to_string(),
1074            });
1075        }
1076        let (_, _, candidate_blob_sha256, _, recovered_blob_sha256) = evidence
1077            .whole_blob_authority_transition()
1078            .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
1079                runtime_id: runtime_id.to_string(),
1080                detail: "WholeBlob recovery evidence has no WholeBlob transition".to_string(),
1081            })?;
1082        if candidate_blob_sha256 != expected.candidate_blob_sha256() {
1083            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1084                runtime_id: runtime_id.to_string(),
1085                detail: "WholeBlob recovery candidate digest changed after classification"
1086                    .to_string(),
1087            });
1088        }
1089        if inner.receipts.contains_key(&receipt_key) {
1090            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1091                runtime_id: runtime_id.to_string(),
1092                detail: "WholeBlob recovery receipt identity already exists".to_string(),
1093            });
1094        }
1095        let lifecycle_matches = match &lifecycle_expected {
1096            MachineLifecycleExpectedVersion::Missing => {
1097                !inner.runtime_lifecycle.contains_key(&runtime_id.0)
1098            }
1099            MachineLifecycleExpectedVersion::Version(version) => inner
1100                .runtime_lifecycle
1101                .get(&runtime_id.0)
1102                .is_some_and(|bytes| {
1103                    super::MachineLifecycleObservationVersion::from_raw_record(bytes) == *version
1104                }),
1105        };
1106        if !lifecycle_matches {
1107            return Err(RuntimeStoreError::MachineLifecycleVersionConflict {
1108                runtime_id: runtime_id.to_string(),
1109            });
1110        }
1111        precheck_fenced_input_updates(inner.input_states.get(&runtime_id.0), &input_updates)?;
1112        let prepared_input_mutations = prepare_memory_input_state_mutations(
1113            &inner,
1114            &runtime_id.0,
1115            input_updates
1116                .iter()
1117                .map(|(target, _)| MemoryInputStateMutation::Upsert(target.clone()))
1118                .collect(),
1119        )?;
1120        let mut enriched_receipts = Vec::new();
1121        for enrichment in evidence.receipt_digest_enrichments() {
1122            let original = enrichment.original_receipt();
1123            let key = ReceiptKey {
1124                runtime_id: runtime_id.0.clone(),
1125                run_id: original.run_id.clone(),
1126                sequence: original.sequence,
1127            };
1128            let current = inner.receipts.get(&key).ok_or_else(|| {
1129                RuntimeStoreError::SessionPersistenceAuthorityConflict {
1130                    runtime_id: runtime_id.to_string(),
1131                    detail: format!(
1132                        "recovery receipt enrichment source {}:{} is absent",
1133                        original.run_id, original.sequence
1134                    ),
1135                }
1136            })?;
1137            let current_bytes = serde_json::to_vec(current)
1138                .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
1139            let source = super::PreparedRecoveryReceiptSource::from_serialized_row(&current_bytes)?;
1140            if source.receipt() != original
1141                || source.exact_row_token() != enrichment.original_exact_row_token()
1142            {
1143                return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1144                    runtime_id: runtime_id.to_string(),
1145                    detail: format!(
1146                        "recovery receipt enrichment source {}:{} changed after classification",
1147                        original.run_id, original.sequence
1148                    ),
1149                });
1150            }
1151            enriched_receipts.push((key, enrichment.enriched_receipt()));
1152        }
1153
1154        let (promoted_bytes, mut catalog_entry, compaction_intents) =
1155            if let Some(repaired_snapshot) = repaired_snapshot {
1156                let (session, serialized, blob_sha256) = repaired_snapshot.into_parts();
1157                if session.id() != expected.session_id() || blob_sha256 != recovered_blob_sha256 {
1158                    return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1159                        runtime_id: runtime_id.to_string(),
1160                        detail: "WholeBlob repaired body differs from recovery evidence"
1161                            .to_string(),
1162                    });
1163                }
1164                let catalog_entry = super::RuntimeSessionCatalogEntry::from_session(
1165                    session.as_ref(),
1166                    RuntimeSessionPersistenceProfile::WholeBlobV1,
1167                    Some(runtime_state),
1168                )?;
1169                let intents = super::validated_compaction_projection_intents(session.as_ref())?;
1170                (serialized.session_snapshot, catalog_entry, intents)
1171            } else {
1172                if recovered_blob_sha256 != expected.candidate_blob_sha256()
1173                    || receipt.conversation_digest.as_deref()
1174                        != Some(stored.conversation_digest.as_str())
1175                    || u64::try_from(receipt.message_count).ok() != Some(stored.message_count)
1176                {
1177                    return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1178                        runtime_id: runtime_id.to_string(),
1179                        detail:
1180                            "completed WholeBlob recovery does not bind the stored candidate facts"
1181                                .to_string(),
1182                    });
1183                }
1184                (
1185                    Arc::clone(&stored.candidate_bytes),
1186                    stored.catalog_entry.clone(),
1187                    stored.compaction_projection_intents.clone(),
1188                )
1189            };
1190        if let Some(existing) = inner.compaction_projection_outbox.get(&runtime_id.0) {
1191            for intent in &compaction_intents {
1192                if let Some(entry) = existing.get(&intent.projection)
1193                    && (entry.finalized || entry.intent != *intent)
1194                {
1195                    return Err(RuntimeStoreError::WriteFailed(format!(
1196                        "WholeBlob recovery conflicts with compaction rewrite {}",
1197                        intent.projection.revision()
1198                    )));
1199                }
1200            }
1201        }
1202        let next = WholeBlobStoreAuthority::issued(
1203            expected.session_id().clone(),
1204            current.store_revision().checked_add(1).ok_or_else(|| {
1205                RuntimeStoreError::WriteFailed(format!(
1206                    "WholeBlob store revision exhausted for runtime {runtime_id}"
1207                ))
1208            })?,
1209            recovered_blob_sha256.to_string(),
1210        )?;
1211        catalog_entry.set_runtime_state(Some(runtime_state));
1212
1213        inner.sessions.insert(runtime_id.0.clone(), promoted_bytes);
1214        inner
1215            .session_authorities
1216            .insert(runtime_id.0.clone(), next.clone());
1217        inner
1218            .session_catalog
1219            .insert(runtime_id.0.clone(), catalog_entry);
1220        let outbox = inner
1221            .compaction_projection_outbox
1222            .entry(runtime_id.0.clone())
1223            .or_default();
1224        for intent in compaction_intents {
1225            outbox
1226                .entry(intent.projection.clone())
1227                .or_insert(CompactionOutboxEntry {
1228                    intent,
1229                    finalized: false,
1230                });
1231        }
1232        inner.whole_blob_provisional_tails.remove(&runtime_id.0);
1233        inner.projection_quarantine.remove(&runtime_id.0);
1234        inner
1235            .runtime_lifecycle
1236            .insert(runtime_id.0.clone(), lifecycle_target);
1237        for (key, enriched) in enriched_receipts {
1238            inner.receipts.insert(key, enriched);
1239        }
1240        inner.receipts.insert(receipt_key, receipt);
1241        apply_prepared_memory_input_state_mutations(
1242            &mut inner,
1243            &runtime_id.0,
1244            prepared_input_mutations,
1245        );
1246        inner.recovery_boundaries.insert(recovery_key, recovery);
1247        Ok(PreparedRuntimeSessionCommitResult::recovery(
1248            super::RuntimeSessionAuthority::WholeBlob(next),
1249            super::RecoveryCommitStatus::Committed,
1250        ))
1251    }
1252}
1253
1254impl Default for InMemoryRuntimeStore {
1255    fn default() -> Self {
1256        Self::new()
1257    }
1258}
1259
1260fn issue_whole_blob_store_authority(
1261    current: Option<&WholeBlobStoreAuthority>,
1262    session_id: &meerkat_core::types::SessionId,
1263    blob_sha256: &str,
1264) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
1265    if let Some(current) = current
1266        && current.session_id() == session_id
1267        && current.blob_sha256() == blob_sha256
1268    {
1269        return Ok(current.clone());
1270    }
1271    let next_revision = current
1272        .map(WholeBlobStoreAuthority::store_revision)
1273        .unwrap_or(0)
1274        .checked_add(1)
1275        .ok_or_else(|| {
1276            RuntimeStoreError::WriteFailed(format!(
1277                "WholeBlob store revision exhausted for session {session_id}"
1278            ))
1279        })?;
1280    WholeBlobStoreAuthority::issued(session_id.clone(), next_revision, blob_sha256.to_string())
1281}
1282
1283fn whole_blob_body_sha256(bytes: &[u8]) -> String {
1284    use sha2::Digest as _;
1285    format!("row-sha256:{:x}", sha2::Sha256::digest(bytes))
1286}
1287
1288fn promote_whole_blob_provisional_locked(
1289    inner: &mut Inner,
1290    runtime_id: &LogicalRuntimeId,
1291    expected: &WholeBlobProvisionalTailAuthority,
1292    receipt: &RunBoundaryReceipt,
1293    checkpoint_conversation_digest: &str,
1294    checkpoint_message_count: u64,
1295    runtime_state: Option<crate::runtime_state::RuntimeState>,
1296) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
1297    let stored = inner
1298        .whole_blob_provisional_tails
1299        .get(&runtime_id.0)
1300        .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
1301            runtime_id: runtime_id.to_string(),
1302            detail: "WholeBlob provisional promotion candidate is absent".to_string(),
1303        })?;
1304    let current = inner
1305        .session_authorities
1306        .get(&runtime_id.0)
1307        .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
1308            runtime_id: runtime_id.to_string(),
1309            detail: "WholeBlob provisional promotion has no committed base".to_string(),
1310        })?;
1311    if &stored.authority != expected
1312        || expected.run_id() != &receipt.run_id
1313        || current.session_id() != expected.session_id()
1314        || current.store_revision() != expected.base_store_revision()
1315        || current.blob_sha256() != expected.base_blob_sha256()
1316    {
1317        return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1318            runtime_id: runtime_id.to_string(),
1319            detail:
1320                "WholeBlob provisional promotion does not exactly match stored base/run/candidate"
1321                    .to_string(),
1322        });
1323    }
1324    if stored.conversation_digest != checkpoint_conversation_digest
1325        || stored.message_count != checkpoint_message_count
1326        || receipt.conversation_digest.as_deref() != Some(stored.conversation_digest.as_str())
1327        || u64::try_from(receipt.message_count).ok() != Some(stored.message_count)
1328    {
1329        return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1330            runtime_id: runtime_id.to_string(),
1331            detail: "WholeBlob final receipt does not bind the stored checkpoint count/digest"
1332                .to_string(),
1333        });
1334    }
1335    if let Some(existing) = inner.compaction_projection_outbox.get(&runtime_id.0) {
1336        for intent in &stored.compaction_projection_intents {
1337            if let Some(entry) = existing.get(&intent.projection)
1338                && (entry.finalized || entry.intent != *intent)
1339            {
1340                return Err(RuntimeStoreError::WriteFailed(format!(
1341                    "WholeBlob provisional promotion conflicts with compaction rewrite {}",
1342                    intent.projection.revision()
1343                )));
1344            }
1345        }
1346    }
1347    let next = WholeBlobStoreAuthority::issued(
1348        expected.session_id().clone(),
1349        current.store_revision().checked_add(1).ok_or_else(|| {
1350            RuntimeStoreError::WriteFailed(format!(
1351                "WholeBlob store revision exhausted for runtime {runtime_id}"
1352            ))
1353        })?,
1354        expected.candidate_blob_sha256().to_string(),
1355    )?;
1356    // The candidate Arc is the sole body allocation. Promotion only moves the
1357    // small authority pointer and reuses that exact store-owned allocation.
1358    let promoted_bytes = Arc::clone(&stored.candidate_bytes);
1359    let mut catalog_entry = stored.catalog_entry.clone();
1360    if let Some(runtime_state) = runtime_state {
1361        catalog_entry.set_runtime_state(Some(runtime_state));
1362    } else if let Some(existing) = inner.session_catalog.get(&runtime_id.0) {
1363        catalog_entry.set_runtime_state(existing.runtime_state());
1364    }
1365    let compaction_projection_intents = stored.compaction_projection_intents.clone();
1366    inner.sessions.insert(runtime_id.0.clone(), promoted_bytes);
1367    inner
1368        .session_authorities
1369        .insert(runtime_id.0.clone(), next.clone());
1370    inner
1371        .session_catalog
1372        .insert(runtime_id.0.clone(), catalog_entry);
1373    let outbox = inner
1374        .compaction_projection_outbox
1375        .entry(runtime_id.0.clone())
1376        .or_default();
1377    for intent in compaction_projection_intents {
1378        outbox
1379            .entry(intent.projection.clone())
1380            .or_insert(CompactionOutboxEntry {
1381                intent,
1382                finalized: false,
1383            });
1384    }
1385    inner.whole_blob_provisional_tails.remove(&runtime_id.0);
1386    inner.projection_quarantine.remove(&runtime_id.0);
1387    Ok(next)
1388}
1389
1390fn commit_prepared_whole_blob_snapshot_locked(
1391    inner: &mut Inner,
1392    runtime_id: &LogicalRuntimeId,
1393    prepared: PreparedWholeBlobSnapshot,
1394) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
1395    let (session, serialized, candidate_blob_sha256) = prepared.into_parts();
1396    if inner
1397        .whole_blob_provisional_tails
1398        .contains_key(&runtime_id.0)
1399    {
1400        return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1401            runtime_id: runtime_id.to_string(),
1402            detail: "ordinary WholeBlob write cannot bypass or re-encode a store-owned provisional candidate; use exact metadata-only promotion".to_string(),
1403        });
1404    }
1405    let next = issue_whole_blob_store_authority(
1406        inner.session_authorities.get(&runtime_id.0),
1407        session.id(),
1408        &candidate_blob_sha256,
1409    )?;
1410    let catalog_entry = super::RuntimeSessionCatalogEntry::from_session(
1411        session.as_ref(),
1412        super::RuntimeSessionPersistenceProfile::WholeBlobV1,
1413        inner
1414            .session_catalog
1415            .get(&runtime_id.0)
1416            .and_then(super::RuntimeSessionCatalogEntry::runtime_state),
1417    )?;
1418    inner
1419        .sessions
1420        .insert(runtime_id.0.clone(), serialized.session_snapshot);
1421    inner
1422        .session_authorities
1423        .insert(runtime_id.0.clone(), next.clone());
1424    inner
1425        .session_catalog
1426        .insert(runtime_id.0.clone(), catalog_entry);
1427    inner.projection_quarantine.remove(&runtime_id.0);
1428    Ok(next)
1429}
1430
1431/// Exact target-local compare token for one stored input bundle. The memory
1432/// store's canonical row bytes are the bundle's serialized form (the same
1433/// encoding the SQLite backend persists), so both backends report and
1434/// enforce the same digests.
1435fn memory_input_row_version_digest(bundle: &StoredInputState) -> Result<String, RuntimeStoreError> {
1436    use sha2::Digest as _;
1437    let bytes = serde_json::to_vec(bundle)
1438        .map_err(|err| RuntimeStoreError::Internal(format!("input row encode failed: {err}")))?;
1439    Ok(format!("sha256:{:x}", sha2::Sha256::digest(&bytes)))
1440}
1441
1442fn prepared_memory_recovery_input_snapshot(
1443    inner: &Inner,
1444    runtime_id: &LogicalRuntimeId,
1445) -> Result<PreparedRecoveryInputSnapshot, RuntimeStoreError> {
1446    let states = inner.input_states.get(&runtime_id.0);
1447    let revision = RecoveryInputSetRevision::from_store_generation(
1448        inner
1449            .recovery_input_set_revisions
1450            .get(&runtime_id.0)
1451            .copied()
1452            .unwrap_or(0),
1453    );
1454    let rows = inner
1455        .recovery_nonterminal_inputs
1456        .get(&runtime_id.0)
1457        .into_iter()
1458        .flatten()
1459        .map(|input_id| {
1460            let bundle = states
1461                .and_then(|states| states.get(&InputId::from_uuid(*input_id)))
1462                .cloned()
1463                .ok_or_else(|| {
1464                    RuntimeStoreError::ReadFailed(format!(
1465                        "recovery nonterminal index for runtime {runtime_id} names missing input {input_id}"
1466                    ))
1467                })?;
1468            let digest = memory_input_row_version_digest(&bundle)?;
1469            Ok((bundle, digest))
1470        })
1471        .collect::<Result<Vec<_>, _>>()?;
1472    PreparedRecoveryInputSnapshot::from_exact_nonterminal_rows(runtime_id.clone(), revision, rows)
1473}
1474
1475/// Pre-validate every fenced input update against the current rows. Must run
1476/// BEFORE any mutation so a stale fence leaves the whole boundary untouched
1477/// (the SQLite backend gets this from its transaction).
1478fn precheck_fenced_input_updates(
1479    states: Option<&IndexMap<meerkat_core::lifecycle::InputId, StoredInputState>>,
1480    input_updates: &[(StoredInputState, Option<String>)],
1481) -> Result<(), RuntimeStoreError> {
1482    for (bundle, expected) in input_updates {
1483        let Some(expected) = expected else { continue };
1484        let current = states.and_then(|map| map.get(&bundle.state.input_id));
1485        let matches = match current {
1486            Some(current) => memory_input_row_version_digest(current)? == *expected,
1487            None => false,
1488        };
1489        if !matches {
1490            return Err(RuntimeStoreError::InputRowVersionConflict {
1491                input_id: bundle.state.input_id.0.to_string(),
1492            });
1493        }
1494    }
1495    Ok(())
1496}
1497
1498/// Deserialize a persisted session-snapshot blob through typed serde, matching
1499/// the SQLite runtime store read path. `Session::deserialize` validates the
1500/// mandatory envelope version against the generated persistence version
1501/// authority, so a missing or non-current (v0/v1) row fails closed instead of
1502/// silently defaulting or upgrading on read.
1503fn deserialize_persisted_session(bytes: &[u8]) -> Result<meerkat_core::Session, RuntimeStoreError> {
1504    serde_json::from_slice(bytes).map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))
1505}
1506
1507fn ensure_compaction_intents_already_outboxed(
1508    inner: &Inner,
1509    runtime_id: &LogicalRuntimeId,
1510    session: &meerkat_core::Session,
1511) -> Result<(), RuntimeStoreError> {
1512    let intents = super::validated_compaction_projection_intents(session)?;
1513    ensure_compaction_intents_already_outboxed_list(inner, runtime_id, &intents)
1514}
1515
1516fn ensure_compaction_intents_already_outboxed_list(
1517    inner: &Inner,
1518    runtime_id: &LogicalRuntimeId,
1519    intents: &[meerkat_core::CompactionProjectionIntent],
1520) -> Result<(), RuntimeStoreError> {
1521    let existing = inner.compaction_projection_outbox.get(&runtime_id.0);
1522    for intent in intents {
1523        match existing.and_then(|entries| entries.get(&intent.projection)) {
1524            Some(entry) if entry.finalized => {
1525                return Err(RuntimeStoreError::WriteFailed(format!(
1526                    "non-boundary snapshot replays finalized compaction intent {}",
1527                    intent.projection.revision()
1528                )));
1529            }
1530            Some(entry) if entry.intent == *intent => {}
1531            Some(_) => {
1532                return Err(RuntimeStoreError::WriteFailed(format!(
1533                    "non-boundary snapshot conflicts with compaction outbox rewrite {}",
1534                    intent.projection.revision()
1535                )));
1536            }
1537            None => {
1538                return Err(RuntimeStoreError::WriteFailed(format!(
1539                    "non-boundary snapshot introduces compaction intent {} without atomic outbox authority",
1540                    intent.projection.revision()
1541                )));
1542            }
1543        }
1544    }
1545    Ok(())
1546}
1547
1548#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1549#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1550impl super::RuntimeSessionAuthorityOps for InMemoryRuntimeStore {
1551    fn session_persistence_profile(&self) -> super::RuntimeSessionPersistenceProfile {
1552        super::RuntimeSessionPersistenceProfile::WholeBlobV1
1553    }
1554
1555    fn session_boundary_authority_read_cost(&self) -> RuntimeSessionAuthorityReadCost {
1556        RuntimeSessionAuthorityReadCost::Bounded
1557    }
1558
1559    async fn write_prepared_head_canonical_provisional_tail(
1560        &self,
1561        _runtime_id: &LogicalRuntimeId,
1562        _prepared: super::PreparedHeadCanonicalProvisionalTail,
1563    ) -> Result<meerkat_core::HeadCanonicalProvisionalTailAuthority, RuntimeStoreError> {
1564        Err(RuntimeStoreError::Unsupported(
1565            "HeadCanonical provisional-tail writes".to_string(),
1566        ))
1567    }
1568
1569    async fn load_head_canonical_provisional_tail(
1570        &self,
1571        _runtime_id: &LogicalRuntimeId,
1572    ) -> Result<Option<meerkat_core::HeadCanonicalProvisionalTailAuthority>, RuntimeStoreError>
1573    {
1574        Err(RuntimeStoreError::Unsupported(
1575            "HeadCanonical provisional-tail observation".to_string(),
1576        ))
1577    }
1578
1579    async fn discard_head_canonical_provisional_tail(
1580        &self,
1581        _runtime_id: &LogicalRuntimeId,
1582        _expected: &meerkat_core::HeadCanonicalProvisionalTailAuthority,
1583    ) -> Result<bool, RuntimeStoreError> {
1584        Err(RuntimeStoreError::Unsupported(
1585            "HeadCanonical provisional-tail discard".to_string(),
1586        ))
1587    }
1588
1589    async fn load_durable_tail_recovery_source(
1590        &self,
1591        _runtime_id: &LogicalRuntimeId,
1592    ) -> Result<Option<super::PreparedDurableTailRecoverySource>, RuntimeStoreError> {
1593        Err(
1594            RuntimeStoreError::PreparedRecoveryRequiresAtomicPhysicalHeadCas {
1595                profile: super::RuntimeSessionPersistenceProfile::WholeBlobV1,
1596            },
1597        )
1598    }
1599
1600    async fn commit_prepared_session_boundary(
1601        &self,
1602        runtime_id: &LogicalRuntimeId,
1603        request: super::PreparedRuntimeSessionCommit,
1604    ) -> Result<super::PreparedRuntimeSessionCommitResult, RuntimeStoreError> {
1605        use super::{
1606            PreparedRuntimeSessionCommitPayload, PreparedRuntimeSessionCommitResult,
1607            RuntimeSessionAuthority, RuntimeSessionPersistenceProfile,
1608        };
1609
1610        let authority = match request.into_payload() {
1611            PreparedRuntimeSessionCommitPayload::SnapshotOnly { session } => {
1612                let prepared = super::prepared_whole_blob_snapshot(&session)?;
1613                Some(
1614                    self.commit_session_snapshot_inner(runtime_id, prepared)
1615                        .await?,
1616                )
1617            }
1618            PreparedRuntimeSessionCommitPayload::Success {
1619                session,
1620                receipt,
1621                input_updates,
1622                session_store_key,
1623            } => {
1624                let prepared = session
1625                    .as_ref()
1626                    .map(super::prepared_whole_blob_snapshot)
1627                    .transpose()?;
1628                self.atomic_apply_prepared_whole_blob(
1629                    runtime_id,
1630                    prepared,
1631                    receipt,
1632                    input_updates,
1633                    session_store_key,
1634                )
1635                .await?
1636            }
1637            PreparedRuntimeSessionCommitPayload::PromoteWholeBlobSuccess {
1638                promotion,
1639                receipt,
1640                input_updates,
1641                session_store_key,
1642            } => Some(
1643                self.atomic_promote_whole_blob(
1644                    runtime_id,
1645                    promotion,
1646                    receipt,
1647                    input_updates,
1648                    session_store_key,
1649                )
1650                .await?,
1651            ),
1652            PreparedRuntimeSessionCommitPayload::ServiceTurnTerminal {
1653                session,
1654                receipt,
1655                machine_lifecycle,
1656                session_store_key,
1657            } => Some(
1658                self.atomic_apply_prepared_whole_blob_with_machine_lifecycle(
1659                    runtime_id,
1660                    super::prepared_whole_blob_snapshot(&session)?,
1661                    receipt,
1662                    machine_lifecycle,
1663                    Vec::new(),
1664                    session_store_key,
1665                )
1666                .await?,
1667            ),
1668            PreparedRuntimeSessionCommitPayload::PromoteWholeBlobServiceTurnTerminal {
1669                promotion,
1670                receipt,
1671                machine_lifecycle,
1672                session_store_key,
1673            } => Some(
1674                self.atomic_promote_whole_blob_with_machine_lifecycle(
1675                    runtime_id,
1676                    promotion,
1677                    receipt,
1678                    machine_lifecycle,
1679                    Vec::new(),
1680                    session_store_key,
1681                )
1682                .await?,
1683            ),
1684            PreparedRuntimeSessionCommitPayload::MachineTerminal {
1685                session,
1686                receipt,
1687                machine_lifecycle,
1688                input_updates,
1689                session_store_key,
1690            } => Some(
1691                self.atomic_apply_prepared_whole_blob_with_machine_lifecycle(
1692                    runtime_id,
1693                    super::prepared_whole_blob_snapshot(&session)?,
1694                    receipt,
1695                    machine_lifecycle,
1696                    input_updates,
1697                    session_store_key,
1698                )
1699                .await?,
1700            ),
1701            PreparedRuntimeSessionCommitPayload::PromoteWholeBlobMachineTerminal {
1702                promotion,
1703                receipt,
1704                machine_lifecycle,
1705                input_updates,
1706                session_store_key,
1707            } => Some(
1708                self.atomic_promote_whole_blob_with_machine_lifecycle(
1709                    runtime_id,
1710                    promotion,
1711                    receipt,
1712                    machine_lifecycle,
1713                    input_updates,
1714                    session_store_key,
1715                )
1716                .await?,
1717            ),
1718            PreparedRuntimeSessionCommitPayload::PromoteWholeBlobRecovery {
1719                promotion,
1720                evidence,
1721                receipt,
1722                machine_lifecycle,
1723                input_updates,
1724                session_store_key,
1725            } => {
1726                return self
1727                    .atomic_recover_whole_blob(
1728                        runtime_id,
1729                        promotion,
1730                        evidence,
1731                        receipt,
1732                        machine_lifecycle,
1733                        input_updates,
1734                        session_store_key,
1735                    )
1736                    .await;
1737            }
1738            PreparedRuntimeSessionCommitPayload::PromoteHeadCanonicalSuccess { .. }
1739            | PreparedRuntimeSessionCommitPayload::PromoteHeadCanonicalServiceTurnTerminal {
1740                ..
1741            }
1742            | PreparedRuntimeSessionCommitPayload::PromoteHeadCanonicalMachineTerminal { .. } => {
1743                return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1744                    runtime_id: runtime_id.to_string(),
1745                    detail:
1746                        "HeadCanonical provisional promotion cannot commit through a WholeBlob store"
1747                            .to_string(),
1748                });
1749            }
1750            PreparedRuntimeSessionCommitPayload::Recovery { .. } => {
1751                return Err(
1752                    RuntimeStoreError::PreparedRecoveryRequiresAtomicPhysicalHeadCas {
1753                        profile: RuntimeSessionPersistenceProfile::WholeBlobV1,
1754                    },
1755                );
1756            }
1757        };
1758        Ok(match authority {
1759            Some(authority) => PreparedRuntimeSessionCommitResult::committed(
1760                RuntimeSessionAuthority::WholeBlob(authority),
1761            ),
1762            None => PreparedRuntimeSessionCommitResult::receipt_only(
1763                RuntimeSessionPersistenceProfile::WholeBlobV1,
1764            ),
1765        })
1766    }
1767
1768    async fn load_durable_tail_recovery_receipts(
1769        &self,
1770        runtime_id: &LogicalRuntimeId,
1771        run_id: &RunId,
1772    ) -> Result<Vec<super::PreparedRecoveryReceiptSource>, RuntimeStoreError> {
1773        let inner = self.inner.lock().await;
1774        let mut receipts = inner
1775            .receipts
1776            .iter()
1777            .filter(|(key, _)| key.runtime_id == runtime_id.0 && key.run_id == *run_id)
1778            .map(|(_, receipt)| {
1779                serde_json::to_vec(receipt)
1780                    .map_err(|error| RuntimeStoreError::ReadFailed(error.to_string()))
1781                    .and_then(|bytes| {
1782                        super::PreparedRecoveryReceiptSource::from_serialized_row(&bytes)
1783                    })
1784            })
1785            .collect::<Result<Vec<_>, _>>()?;
1786        receipts.sort_by_key(|source| source.receipt().sequence);
1787        Ok(receipts)
1788    }
1789
1790    async fn load_committed_recovery_boundary(
1791        &self,
1792        runtime_id: &LogicalRuntimeId,
1793        candidate_id: &str,
1794    ) -> Result<Option<super::CommittedRecoveryBoundary>, RuntimeStoreError> {
1795        Ok(self
1796            .inner
1797            .lock()
1798            .await
1799            .recovery_boundaries
1800            .get(&(runtime_id.0.clone(), candidate_id.to_string()))
1801            .cloned())
1802    }
1803
1804    async fn load_whole_blob_store_authority(
1805        &self,
1806        runtime_id: &LogicalRuntimeId,
1807    ) -> Result<Option<WholeBlobStoreAuthority>, RuntimeStoreError> {
1808        Ok(self
1809            .inner
1810            .lock()
1811            .await
1812            .session_authorities
1813            .get(&runtime_id.0)
1814            .cloned())
1815    }
1816
1817    async fn load_session_boundary_authority(
1818        &self,
1819        runtime_id: &LogicalRuntimeId,
1820    ) -> Result<Option<RuntimeSessionAuthority>, RuntimeStoreError> {
1821        super::RuntimeSessionAuthorityOps::load_whole_blob_store_authority(self, runtime_id)
1822            .await
1823            .map(|authority| authority.map(RuntimeSessionAuthority::WholeBlob))
1824    }
1825
1826    async fn delete_runtime_session_catalog_entry(
1827        &self,
1828        runtime_id: &LogicalRuntimeId,
1829    ) -> Result<(), RuntimeStoreError> {
1830        self.inner
1831            .lock()
1832            .await
1833            .session_catalog
1834            .remove(&runtime_id.0);
1835        Ok(())
1836    }
1837
1838    async fn load_runtime_session_catalog_entry(
1839        &self,
1840        runtime_id: &LogicalRuntimeId,
1841    ) -> Result<Option<super::RuntimeSessionCatalogEntry>, RuntimeStoreError> {
1842        Ok(self
1843            .inner
1844            .lock()
1845            .await
1846            .session_catalog
1847            .get(&runtime_id.0)
1848            .cloned())
1849    }
1850
1851    async fn list_runtime_session_catalog_entries(
1852        &self,
1853        filter: meerkat_core::SessionFilter,
1854    ) -> Result<Vec<super::RuntimeSessionCatalogEntry>, RuntimeStoreError> {
1855        let inner = self.inner.lock().await;
1856        let mut entries = inner
1857            .session_catalog
1858            .values()
1859            .filter(|entry| {
1860                filter
1861                    .created_after
1862                    .is_none_or(|after| entry.created_at() >= after)
1863                    && filter
1864                        .updated_after
1865                        .is_none_or(|after| entry.updated_at() >= after)
1866            })
1867            .cloned()
1868            .collect::<Vec<_>>();
1869        entries.sort_by(|left, right| {
1870            right.updated_at().cmp(&left.updated_at()).then_with(|| {
1871                left.session_id()
1872                    .to_string()
1873                    .cmp(&right.session_id().to_string())
1874            })
1875        });
1876        let offset = filter.offset.unwrap_or(0).min(entries.len());
1877        let limit = filter.limit.unwrap_or(usize::MAX);
1878        Ok(entries.into_iter().skip(offset).take(limit).collect())
1879    }
1880
1881    async fn load_committed_whole_blob_snapshot(
1882        &self,
1883        runtime_id: &LogicalRuntimeId,
1884    ) -> Result<Option<CommittedWholeBlobSnapshot>, RuntimeStoreError> {
1885        let inner = self.inner.lock().await;
1886        match (
1887            inner.sessions.get(&runtime_id.0),
1888            inner.session_authorities.get(&runtime_id.0),
1889        ) {
1890            (None, None) => Ok(None),
1891            (Some(bytes), Some(authority)) => Ok(Some(CommittedWholeBlobSnapshot::new(
1892                Arc::clone(bytes),
1893                authority.clone(),
1894            )?)),
1895            _ => Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1896                runtime_id: runtime_id.to_string(),
1897                detail: "WholeBlob body and store authority ledger disagree on row presence"
1898                    .to_string(),
1899            }),
1900        }
1901    }
1902
1903    async fn commit_prepared_whole_blob_snapshot_cas(
1904        &self,
1905        runtime_id: &LogicalRuntimeId,
1906        prepared: PreparedWholeBlobSnapshotCas,
1907    ) -> Result<WholeBlobSnapshotCasOutcome, RuntimeStoreError> {
1908        let (expected, candidate_session, candidate_bytes, candidate_blob_sha256) =
1909            prepared.into_parts();
1910        if &LogicalRuntimeId::for_session(candidate_session.id()) != runtime_id
1911            || candidate_session.id() != expected.session_id()
1912        {
1913            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1914                runtime_id: runtime_id.to_string(),
1915                detail: "prepared WholeBlob snapshot CAS does not bind this runtime/session"
1916                    .to_string(),
1917            });
1918        }
1919        let compaction_projection_intents =
1920            super::validated_compaction_projection_intents(candidate_session.as_ref())?;
1921        let mut inner = self.inner.lock().await;
1922        let Some(current) = inner.session_authorities.get(&runtime_id.0) else {
1923            return Ok(WholeBlobSnapshotCasOutcome::Conflict);
1924        };
1925        if current != &expected {
1926            return Ok(WholeBlobSnapshotCasOutcome::Conflict);
1927        }
1928        if current.blob_sha256() == candidate_blob_sha256 {
1929            return Ok(WholeBlobSnapshotCasOutcome::Committed(current.clone()));
1930        }
1931        if inner
1932            .whole_blob_provisional_tails
1933            .contains_key(&runtime_id.0)
1934        {
1935            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1936                runtime_id: runtime_id.to_string(),
1937                detail: "snapshot CAS cannot bypass a store-owned WholeBlob provisional candidate"
1938                    .to_string(),
1939            });
1940        }
1941        ensure_compaction_intents_already_outboxed_list(
1942            &inner,
1943            runtime_id,
1944            &compaction_projection_intents,
1945        )?;
1946        let runtime_state = inner
1947            .session_catalog
1948            .get(&runtime_id.0)
1949            .and_then(super::RuntimeSessionCatalogEntry::runtime_state);
1950        let catalog_entry = super::RuntimeSessionCatalogEntry::from_session(
1951            candidate_session.as_ref(),
1952            super::RuntimeSessionPersistenceProfile::WholeBlobV1,
1953            runtime_state,
1954        )?;
1955        let authority = issue_whole_blob_store_authority(
1956            Some(&expected),
1957            candidate_session.id(),
1958            &candidate_blob_sha256,
1959        )?;
1960        inner.sessions.insert(runtime_id.0.clone(), candidate_bytes);
1961        inner
1962            .session_authorities
1963            .insert(runtime_id.0.clone(), authority.clone());
1964        inner
1965            .session_catalog
1966            .insert(runtime_id.0.clone(), catalog_entry);
1967        inner.projection_quarantine.remove(&runtime_id.0);
1968        Ok(WholeBlobSnapshotCasOutcome::Committed(authority))
1969    }
1970
1971    async fn write_prepared_whole_blob_provisional_tail(
1972        &self,
1973        runtime_id: &LogicalRuntimeId,
1974        prepared: PreparedWholeBlobProvisionalTail,
1975    ) -> Result<WholeBlobProvisionalTailAuthority, RuntimeStoreError> {
1976        let (
1977            authority,
1978            candidate_artifact,
1979            conversation_digest,
1980            message_count,
1981            catalog_entry,
1982            compaction_projection_intents,
1983        ) = prepared.into_parts();
1984        let candidate_bytes = candidate_artifact.bytes_arc();
1985        if &LogicalRuntimeId::for_session(authority.session_id()) != runtime_id
1986            || catalog_entry.session_id() != authority.session_id()
1987            || catalog_entry.persistence_profile()
1988                != super::RuntimeSessionPersistenceProfile::WholeBlobV1
1989            || u64::try_from(catalog_entry.message_count()).ok() != Some(message_count)
1990            || candidate_artifact.row_sha256_token() != authority.candidate_blob_sha256()
1991        {
1992            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
1993                runtime_id: runtime_id.to_string(),
1994                detail: "WholeBlob provisional artifact/catalog does not bind this runtime/session authority"
1995                    .to_string(),
1996            });
1997        }
1998        let mut inner = self.inner.lock().await;
1999        let current = inner
2000            .session_authorities
2001            .get(&runtime_id.0)
2002            .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
2003                runtime_id: runtime_id.to_string(),
2004                detail: "WholeBlob provisional candidate has no committed base".to_string(),
2005            })?;
2006        if current.session_id() != authority.session_id()
2007            || current.store_revision() != authority.base_store_revision()
2008            || current.blob_sha256() != authority.base_blob_sha256()
2009        {
2010            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
2011                runtime_id: runtime_id.to_string(),
2012                detail: "WholeBlob provisional candidate base is stale".to_string(),
2013            });
2014        }
2015        if let Some(existing) = inner.whole_blob_provisional_tails.get(&runtime_id.0) {
2016            if existing.authority == authority {
2017                if existing.conversation_digest != conversation_digest
2018                    || existing.message_count != message_count
2019                {
2020                    return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
2021                        runtime_id: runtime_id.to_string(),
2022                        detail: "WholeBlob provisional retry changes bounded candidate facts"
2023                            .to_string(),
2024                    });
2025                }
2026                return Ok(existing.authority.clone());
2027            }
2028            let required_sequence = existing
2029                .authority
2030                .candidate_sequence()
2031                .checked_add(1)
2032                .ok_or_else(|| {
2033                    RuntimeStoreError::WriteFailed(
2034                        "WholeBlob provisional candidate sequence exhausted".to_string(),
2035                    )
2036                })?;
2037            if existing.authority.session_id() != authority.session_id()
2038                || existing.authority.base_store_revision() != authority.base_store_revision()
2039                || existing.authority.base_blob_sha256() != authority.base_blob_sha256()
2040                || existing.authority.run_id() != authority.run_id()
2041                || authority.candidate_sequence() != required_sequence
2042            {
2043                return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
2044                    runtime_id: runtime_id.to_string(),
2045                    detail: "WholeBlob provisional replacement is stale or skips sequence"
2046                        .to_string(),
2047                });
2048            }
2049        } else if authority.candidate_sequence() != 1 {
2050            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
2051                runtime_id: runtime_id.to_string(),
2052                detail: "first WholeBlob provisional candidate sequence must be one".to_string(),
2053            });
2054        }
2055        inner.whole_blob_provisional_tails.insert(
2056            runtime_id.0.clone(),
2057            StoredWholeBlobProvisionalTail {
2058                authority: authority.clone(),
2059                candidate_bytes,
2060                conversation_digest,
2061                message_count,
2062                catalog_entry,
2063                compaction_projection_intents,
2064            },
2065        );
2066        Ok(authority)
2067    }
2068
2069    async fn load_whole_blob_provisional_tail(
2070        &self,
2071        runtime_id: &LogicalRuntimeId,
2072    ) -> Result<Option<CommittedWholeBlobProvisionalTail>, RuntimeStoreError> {
2073        let inner = self.inner.lock().await;
2074        let Some(stored) = inner.whole_blob_provisional_tails.get(&runtime_id.0) else {
2075            return Ok(None);
2076        };
2077        if whole_blob_body_sha256(stored.candidate_bytes.as_ref())
2078            != stored.authority.candidate_blob_sha256()
2079        {
2080            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
2081                runtime_id: runtime_id.to_string(),
2082                detail: "WholeBlob provisional body digest differs from store authority"
2083                    .to_string(),
2084            });
2085        }
2086        Ok(Some(CommittedWholeBlobProvisionalTail::new(
2087            stored.authority.clone(),
2088            Arc::clone(&stored.candidate_bytes),
2089        )))
2090    }
2091
2092    async fn discard_whole_blob_provisional_tail(
2093        &self,
2094        runtime_id: &LogicalRuntimeId,
2095        expected: &WholeBlobProvisionalTailAuthority,
2096    ) -> Result<bool, RuntimeStoreError> {
2097        let mut inner = self.inner.lock().await;
2098        if inner
2099            .whole_blob_provisional_tails
2100            .get(&runtime_id.0)
2101            .is_some_and(|stored| &stored.authority == expected)
2102        {
2103            inner.whole_blob_provisional_tails.remove(&runtime_id.0);
2104            return Ok(true);
2105        }
2106        Ok(false)
2107    }
2108}
2109
2110#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2111#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2112impl RuntimeStore for InMemoryRuntimeStore {
2113    fn session_authority_ops(&self) -> &dyn super::RuntimeSessionAuthorityOps {
2114        self
2115    }
2116
2117    fn supports_compaction_projection_outbox(&self) -> bool {
2118        true
2119    }
2120
2121    fn input_state_batch_cas_implementation_profile(
2122        &self,
2123    ) -> InputStateBatchCasImplementationProfile {
2124        InputStateBatchCasImplementationProfile::MultiWriter
2125    }
2126
2127    async fn load_runtime_delivery_authority(
2128        &self,
2129        runtime_id: &LogicalRuntimeId,
2130    ) -> Result<Option<RuntimeDeliveryAuthorityRecord>, RuntimeStoreError> {
2131        Ok(self
2132            .inner
2133            .lock()
2134            .await
2135            .runtime_delivery_authority
2136            .get(&runtime_id.0)
2137            .cloned())
2138    }
2139
2140    async fn load_runtime_delivery_record(
2141        &self,
2142        runtime_id: &LogicalRuntimeId,
2143        delivery_id: &str,
2144    ) -> Result<Option<RuntimeDeliveryStoreRecord>, RuntimeStoreError> {
2145        Ok(self
2146            .inner
2147            .lock()
2148            .await
2149            .runtime_delivery_records
2150            .get(&runtime_id.0)
2151            .and_then(|records| {
2152                records
2153                    .values()
2154                    .find(|record| record.delivery_id() == delivery_id)
2155            })
2156            .cloned())
2157    }
2158
2159    async fn compare_and_swap_runtime_delivery_authority(
2160        &self,
2161        runtime_id: &LogicalRuntimeId,
2162        expected_revision: Option<u64>,
2163        replacement: RuntimeDeliveryAuthorityRecord,
2164        inserted_delivery: Option<RuntimeDeliveryStoreRecord>,
2165    ) -> Result<RuntimeDeliveryAuthorityCasOutcome, RuntimeStoreError> {
2166        let mut inner = self.inner.lock().await;
2167        let current = inner.runtime_delivery_authority.get(&runtime_id.0).cloned();
2168        if current
2169            .as_ref()
2170            .map(RuntimeDeliveryAuthorityRecord::revision)
2171            != expected_revision
2172        {
2173            return Ok(RuntimeDeliveryAuthorityCasOutcome::Conflict(current));
2174        }
2175        let required_revision = expected_revision
2176            .map_or(Some(1), |revision| revision.checked_add(1))
2177            .ok_or_else(|| {
2178                RuntimeStoreError::WriteFailed(
2179                    "runtime delivery authority revision exhausted u64".into(),
2180                )
2181            })?;
2182        if replacement.revision() != required_revision {
2183            return Err(RuntimeStoreError::WriteFailed(format!(
2184                "runtime delivery replacement revision {} is not required successor {required_revision}",
2185                replacement.revision()
2186            )));
2187        }
2188        if let Some(record) = inserted_delivery.as_ref() {
2189            let records = inner
2190                .runtime_delivery_records
2191                .entry(runtime_id.0.clone())
2192                .or_default();
2193            if records.contains_key(&record.sequence())
2194                || records
2195                    .values()
2196                    .any(|existing| existing.delivery_id() == record.delivery_id())
2197            {
2198                return Err(RuntimeStoreError::WriteFailed(format!(
2199                    "runtime delivery row {} / sequence {} already exists",
2200                    record.delivery_id(),
2201                    record.sequence()
2202                )));
2203            }
2204        }
2205
2206        inner
2207            .runtime_delivery_authority
2208            .insert(runtime_id.0.clone(), replacement.clone());
2209        if let Some(record) = inserted_delivery {
2210            inner
2211                .runtime_delivery_records
2212                .entry(runtime_id.0.clone())
2213                .or_default()
2214                .insert(record.sequence(), record);
2215        }
2216        Ok(RuntimeDeliveryAuthorityCasOutcome::Applied(replacement))
2217    }
2218
2219    async fn list_runtime_delivery_records(
2220        &self,
2221        runtime_id: &LogicalRuntimeId,
2222        after_sequence: u64,
2223        limit: usize,
2224    ) -> Result<Vec<RuntimeDeliveryStoreRecord>, RuntimeStoreError> {
2225        if limit == 0 {
2226            return Ok(Vec::new());
2227        }
2228        Ok(self
2229            .inner
2230            .lock()
2231            .await
2232            .runtime_delivery_records
2233            .get(&runtime_id.0)
2234            .into_iter()
2235            .flat_map(|records| {
2236                records
2237                    .range((
2238                        std::ops::Bound::Excluded(after_sequence),
2239                        std::ops::Bound::Unbounded,
2240                    ))
2241                    .take(limit)
2242                    .map(|(_, record)| record.clone())
2243            })
2244            .collect())
2245    }
2246
2247    fn persist_auth_oauth_flow_snapshot(
2248        &self,
2249        snapshot_json: &[u8],
2250    ) -> Result<(), RuntimeStoreError> {
2251        *self
2252            .auth_oauth_flow_snapshot
2253            .lock()
2254            .map_err(|err| RuntimeStoreError::WriteFailed(err.to_string()))? =
2255            Some(snapshot_json.to_vec());
2256        Ok(())
2257    }
2258
2259    fn load_auth_oauth_flow_snapshot(&self) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
2260        self.auth_oauth_flow_snapshot
2261            .lock()
2262            .map(|snapshot| snapshot.clone())
2263            .map_err(|err| RuntimeStoreError::ReadFailed(err.to_string()))
2264    }
2265
2266    fn update_auth_oauth_flow_snapshot(
2267        &self,
2268        update: &mut AuthOAuthFlowSnapshotUpdate<'_>,
2269    ) -> Result<(), RuntimeStoreError> {
2270        let mut snapshot = self
2271            .auth_oauth_flow_snapshot
2272            .lock()
2273            .map_err(|err| RuntimeStoreError::WriteFailed(err.to_string()))?;
2274        let next = update(snapshot.as_deref())?;
2275        *snapshot = Some(next);
2276        Ok(())
2277    }
2278
2279    async fn commit_session_snapshot(
2280        &self,
2281        runtime_id: &LogicalRuntimeId,
2282        session_delta: SerializedSessionSnapshot,
2283    ) -> Result<(), RuntimeStoreError> {
2284        let prepared = parsed_whole_blob_snapshot(session_delta)?;
2285        self.commit_session_snapshot_inner(runtime_id, prepared)
2286            .await
2287            .map(|_| ())
2288    }
2289
2290    async fn commit_prepared_whole_blob_rewrite_boundary(
2291        &self,
2292        runtime_id: &LogicalRuntimeId,
2293        boundary: PreparedWholeBlobRewriteStoreParts,
2294    ) -> Result<WholeBlobStoreAuthority, RuntimeStoreError> {
2295        let (
2296            expected,
2297            successor_session_id,
2298            successor_blob_sha256,
2299            successor_bytes,
2300            mut successor_catalog_entry,
2301            compaction_projection_intents,
2302        ) = boundary.into_tuple();
2303        if expected.session_id() != &successor_session_id
2304            || &LogicalRuntimeId::for_session(&successor_session_id) != runtime_id
2305        {
2306            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
2307                runtime_id: runtime_id.to_string(),
2308                detail: "prepared WholeBlob rewrite authorities do not bind this runtime/session"
2309                    .to_string(),
2310            });
2311        }
2312        let mut inner = self.inner.lock().await;
2313        let current = inner
2314            .session_authorities
2315            .get(&runtime_id.0)
2316            .ok_or_else(|| RuntimeStoreError::SessionPersistenceAuthorityConflict {
2317                runtime_id: runtime_id.to_string(),
2318                detail: "prepared WholeBlob predecessor authority is absent".to_string(),
2319            })?;
2320        ensure_compaction_intents_already_outboxed_list(
2321            &inner,
2322            runtime_id,
2323            &compaction_projection_intents,
2324        )?;
2325        let successor_revision = expected.store_revision().checked_add(1).ok_or_else(|| {
2326            RuntimeStoreError::WriteFailed(format!(
2327                "WholeBlob store revision exhausted for runtime {runtime_id}"
2328            ))
2329        })?;
2330        let exact_idempotent_successor = current.session_id() == &successor_session_id
2331            && current.blob_sha256() == successor_blob_sha256
2332            && ((current == &expected && expected.blob_sha256() == successor_blob_sha256)
2333                || current.store_revision() == successor_revision);
2334        if exact_idempotent_successor {
2335            return Ok(current.clone());
2336        }
2337        if current != &expected {
2338            return Err(RuntimeStoreError::SessionPersistenceAuthorityConflict {
2339                runtime_id: runtime_id.to_string(),
2340                detail:
2341                    "prepared WholeBlob predecessor revision/token does not match current authority"
2342                        .to_string(),
2343            });
2344        }
2345        // `PreparedWholeBlobRewriteStoreParts` is non-constructible outside the
2346        // core preparation module: its successor token was derived from these
2347        // exact shared bytes once before the CAS. Re-hashing the complete
2348        // successor here would turn the single prepared final-document hash
2349        // into a second O(document) pass without adding store-local authority.
2350        // This lock still owns the only fact a backend must revalidate: the
2351        // exact current predecessor (or exact already-landed successor).
2352        successor_catalog_entry.set_runtime_state(
2353            inner
2354                .session_catalog
2355                .get(&runtime_id.0)
2356                .and_then(super::RuntimeSessionCatalogEntry::runtime_state),
2357        );
2358        inner.sessions.insert(runtime_id.0.clone(), successor_bytes);
2359        let successor = WholeBlobStoreAuthority::issued(
2360            successor_session_id,
2361            successor_revision,
2362            successor_blob_sha256,
2363        )?;
2364        inner
2365            .session_authorities
2366            .insert(runtime_id.0.clone(), successor.clone());
2367        inner
2368            .session_catalog
2369            .insert(runtime_id.0.clone(), successor_catalog_entry);
2370        inner.projection_quarantine.remove(&runtime_id.0);
2371        Ok(successor)
2372    }
2373
2374    async fn atomic_apply(
2375        &self,
2376        runtime_id: &LogicalRuntimeId,
2377        session_delta: Option<SerializedSessionSnapshot>,
2378        receipt: RunBoundaryReceipt,
2379        input_updates: Vec<InputStatePersistenceRecord>,
2380        session_store_key: Option<meerkat_core::types::SessionId>,
2381    ) -> Result<(), RuntimeStoreError> {
2382        let prepared = session_delta.map(parsed_whole_blob_snapshot).transpose()?;
2383        self.atomic_apply_prepared_whole_blob(
2384            runtime_id,
2385            prepared,
2386            receipt,
2387            input_updates,
2388            session_store_key,
2389        )
2390        .await
2391        .map(|_| ())
2392    }
2393
2394    async fn load_pending_compaction_projections(
2395        &self,
2396        runtime_id: &LogicalRuntimeId,
2397    ) -> Result<Vec<meerkat_core::CompactionProjectionIntent>, RuntimeStoreError> {
2398        let inner = self.inner.lock().await;
2399        let mut pending = inner
2400            .compaction_projection_outbox
2401            .get(&runtime_id.0)
2402            .into_iter()
2403            .flat_map(HashMap::values)
2404            .filter(|entry| !entry.finalized)
2405            .map(|entry| entry.intent.clone())
2406            .collect::<Vec<_>>();
2407        pending.sort_by(|left, right| {
2408            left.projection
2409                .session_id()
2410                .to_string()
2411                .cmp(&right.projection.session_id().to_string())
2412                .then_with(|| {
2413                    left.projection
2414                        .parent_revision()
2415                        .cmp(right.projection.parent_revision())
2416                })
2417                .then_with(|| left.projection.revision().cmp(right.projection.revision()))
2418                .then_with(|| {
2419                    left.projection
2420                        .commit_fingerprint()
2421                        .cmp(right.projection.commit_fingerprint())
2422                })
2423        });
2424        Ok(pending)
2425    }
2426
2427    async fn mark_compaction_projection_finalized(
2428        &self,
2429        runtime_id: &LogicalRuntimeId,
2430        projection: &meerkat_core::CompactionProjectionId,
2431    ) -> Result<(), RuntimeStoreError> {
2432        let mut inner = self.inner.lock().await;
2433        let outbox_exists = inner
2434            .compaction_projection_outbox
2435            .get(&runtime_id.0)
2436            .is_some_and(|entries| entries.contains_key(projection));
2437        if !outbox_exists {
2438            return Err(RuntimeStoreError::NotFound(format!(
2439                "compaction outbox rewrite {}",
2440                projection.revision()
2441            )));
2442        }
2443        let cleaned_snapshot = inner
2444            .sessions
2445            .get(&runtime_id.0)
2446            .map(|snapshot| {
2447                let mut session = deserialize_persisted_session(snapshot)?;
2448                complete_compaction_projection_intent(&mut session, projection)?;
2449                let artifact = session
2450                    .to_persisted_artifact()
2451                    .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
2452                let authority = issue_whole_blob_store_authority(
2453                    inner.session_authorities.get(&runtime_id.0),
2454                    session.id(),
2455                    artifact.row_sha256_token(),
2456                )?;
2457                Ok((artifact.bytes_arc(), authority))
2458            })
2459            .transpose()?;
2460        let entry = inner
2461            .compaction_projection_outbox
2462            .get_mut(&runtime_id.0)
2463            .and_then(|entries| entries.get_mut(projection))
2464            .ok_or_else(|| {
2465                RuntimeStoreError::NotFound(format!(
2466                    "compaction outbox rewrite {}",
2467                    projection.revision()
2468                ))
2469            })?;
2470        entry.finalized = true;
2471        if let Some((cleaned_snapshot, authority)) = cleaned_snapshot {
2472            inner
2473                .sessions
2474                .insert(runtime_id.0.clone(), cleaned_snapshot);
2475            inner
2476                .session_authorities
2477                .insert(runtime_id.0.clone(), authority);
2478        }
2479        Ok(())
2480    }
2481
2482    async fn atomic_apply_with_machine_lifecycle(
2483        &self,
2484        runtime_id: &LogicalRuntimeId,
2485        session_delta: SerializedSessionSnapshot,
2486        receipt: RunBoundaryReceipt,
2487        machine_lifecycle: MachineLifecycleCommit,
2488        input_updates: Vec<InputStatePersistenceRecord>,
2489        session_store_key: meerkat_core::types::SessionId,
2490    ) -> Result<(), RuntimeStoreError> {
2491        let prepared = parsed_whole_blob_snapshot(session_delta)?;
2492        self.atomic_apply_prepared_whole_blob_with_machine_lifecycle(
2493            runtime_id,
2494            prepared,
2495            receipt,
2496            machine_lifecycle,
2497            input_updates,
2498            session_store_key,
2499        )
2500        .await
2501        .map(|_| ())
2502    }
2503
2504    async fn load_input_states(
2505        &self,
2506        runtime_id: &LogicalRuntimeId,
2507    ) -> Result<Vec<InputStateRow>, RuntimeStoreError> {
2508        let inner = self.inner.lock().await;
2509        let states = inner
2510            .input_states
2511            .get(&runtime_id.0)
2512            .map(|m| {
2513                m.values()
2514                    .cloned()
2515                    .map(|state| InputStateRow::Decoded(Box::new(state)))
2516                    .collect()
2517            })
2518            .unwrap_or_default();
2519        Ok(states)
2520    }
2521
2522    async fn load_boundary_receipt(
2523        &self,
2524        runtime_id: &LogicalRuntimeId,
2525        run_id: &RunId,
2526        sequence: u64,
2527    ) -> Result<Option<RunBoundaryReceipt>, RuntimeStoreError> {
2528        let inner = self.inner.lock().await;
2529        let key = ReceiptKey {
2530            runtime_id: runtime_id.0.clone(),
2531            run_id: run_id.clone(),
2532            sequence,
2533        };
2534        Ok(inner.receipts.get(&key).cloned())
2535    }
2536
2537    async fn load_committed_boundary_receipts(
2538        &self,
2539        runtime_id: &LogicalRuntimeId,
2540        run_id: &RunId,
2541    ) -> Result<Vec<RunBoundaryReceipt>, RuntimeStoreError> {
2542        let inner = self.inner.lock().await;
2543        let mut receipts = inner
2544            .receipts
2545            .iter()
2546            .filter(|(key, _)| key.runtime_id == runtime_id.0 && key.run_id == *run_id)
2547            .map(|(_, receipt)| receipt.clone())
2548            .collect::<Vec<_>>();
2549        receipts.sort_by_key(|receipt| receipt.sequence);
2550        Ok(receipts)
2551    }
2552
2553    async fn load_input_states_with_versions(
2554        &self,
2555        runtime_id: &LogicalRuntimeId,
2556    ) -> Result<PreparedRecoveryInputSnapshot, RuntimeStoreError> {
2557        let inner = self.inner.lock().await;
2558        prepared_memory_recovery_input_snapshot(&inner, runtime_id)
2559    }
2560
2561    async fn load_session_snapshot(
2562        &self,
2563        runtime_id: &LogicalRuntimeId,
2564    ) -> Result<Option<Arc<Vec<u8>>>, RuntimeStoreError> {
2565        let inner = self.inner.lock().await;
2566        Ok(inner.sessions.get(&runtime_id.0).cloned())
2567    }
2568
2569    async fn clear_session_snapshot(
2570        &self,
2571        runtime_id: &LogicalRuntimeId,
2572    ) -> Result<(), RuntimeStoreError> {
2573        let mut inner = self.inner.lock().await;
2574        inner.sessions.remove(&runtime_id.0);
2575        inner.session_authorities.remove(&runtime_id.0);
2576        inner.session_catalog.remove(&runtime_id.0);
2577        inner.whole_blob_provisional_tails.remove(&runtime_id.0);
2578        Ok(())
2579    }
2580
2581    async fn replace_session_snapshot_if_current(
2582        &self,
2583        runtime_id: &LogicalRuntimeId,
2584        expected_current: &[u8],
2585        replacement: Vec<u8>,
2586    ) -> Result<bool, RuntimeStoreError> {
2587        let replacement = parsed_whole_blob_snapshot(SerializedSessionSnapshot {
2588            session_snapshot: Arc::new(replacement),
2589        })?;
2590        let (replacement_session, replacement, blob_sha256) = replacement.into_parts();
2591        let mut inner = self.inner.lock().await;
2592        let Some(current) = inner.sessions.get(&runtime_id.0) else {
2593            return Ok(false);
2594        };
2595        if current.as_ref() != expected_current {
2596            return Ok(false);
2597        }
2598        ensure_compaction_intents_already_outboxed(
2599            &inner,
2600            runtime_id,
2601            replacement_session.as_ref(),
2602        )?;
2603        let authority = issue_whole_blob_store_authority(
2604            inner.session_authorities.get(&runtime_id.0),
2605            replacement_session.id(),
2606            &blob_sha256,
2607        )?;
2608        inner
2609            .sessions
2610            .insert(runtime_id.0.clone(), replacement.session_snapshot);
2611        inner
2612            .session_authorities
2613            .insert(runtime_id.0.clone(), authority);
2614        inner.projection_quarantine.remove(&runtime_id.0);
2615        Ok(true)
2616    }
2617
2618    async fn clear_session_snapshot_if_current(
2619        &self,
2620        runtime_id: &LogicalRuntimeId,
2621        expected_current: &[u8],
2622    ) -> Result<bool, RuntimeStoreError> {
2623        let mut inner = self.inner.lock().await;
2624        let Some(current) = inner.sessions.get(&runtime_id.0) else {
2625            return Ok(false);
2626        };
2627        if current.as_ref() != expected_current {
2628            return Ok(false);
2629        }
2630        inner.sessions.remove(&runtime_id.0);
2631        inner.session_authorities.remove(&runtime_id.0);
2632        inner.session_catalog.remove(&runtime_id.0);
2633        inner.whole_blob_provisional_tails.remove(&runtime_id.0);
2634        // Record the in-memory quarantine marker atomically with the snapshot
2635        // removal, mirroring the durable SQLite path.
2636        inner.projection_quarantine.insert(runtime_id.0.clone());
2637        Ok(true)
2638    }
2639
2640    async fn is_runtime_projection_quarantined(
2641        &self,
2642        runtime_id: &LogicalRuntimeId,
2643    ) -> Result<bool, RuntimeStoreError> {
2644        let inner = self.inner.lock().await;
2645        Ok(inner.projection_quarantine.contains(&runtime_id.0))
2646    }
2647
2648    async fn persist_input_state(
2649        &self,
2650        runtime_id: &LogicalRuntimeId,
2651        state: &InputStatePersistenceRecord,
2652    ) -> Result<(), RuntimeStoreError> {
2653        let mut inner = self.inner.lock().await;
2654        let update = (
2655            state.clone_stored(),
2656            state.expected_row_digest().map(str::to_owned),
2657        );
2658        precheck_fenced_input_updates(
2659            inner.input_states.get(&runtime_id.0),
2660            std::slice::from_ref(&update),
2661        )?;
2662        let (bundle, _expected) = update;
2663        store_input_state(&mut inner, &runtime_id.0, bundle)
2664    }
2665
2666    async fn persist_input_states_atomically(
2667        &self,
2668        runtime_id: &LogicalRuntimeId,
2669        records: &[InputStatePersistenceRecord],
2670    ) -> Result<(), RuntimeStoreError> {
2671        let mut inner = self.inner.lock().await;
2672        let updates = records
2673            .iter()
2674            .map(|record| {
2675                (
2676                    record.clone_stored(),
2677                    record.expected_row_digest().map(str::to_owned),
2678                )
2679            })
2680            .collect::<Vec<_>>();
2681        precheck_fenced_input_updates(inner.input_states.get(&runtime_id.0), &updates)?;
2682        let prepared = prepare_memory_input_state_mutations(
2683            &inner,
2684            &runtime_id.0,
2685            updates
2686                .into_iter()
2687                .map(|(bundle, _expected)| MemoryInputStateMutation::Upsert(bundle))
2688                .collect(),
2689        )?;
2690        apply_prepared_memory_input_state_mutations(&mut inner, &runtime_id.0, prepared);
2691        Ok(())
2692    }
2693
2694    async fn compare_and_swap_input_states_atomically(
2695        &self,
2696        runtime_id: &LogicalRuntimeId,
2697        expected: &[StoredInputState],
2698        replacements: &[InputStatePersistenceRecord],
2699    ) -> Result<InputStateBatchCasOutcome, RuntimeStoreError> {
2700        // Serialize and validate the full request before taking the mutation
2701        // lock, so no fallible request preparation can occur after writes.
2702        let prepared = prepare_input_state_batch_cas(expected, replacements)?;
2703        if prepared.is_empty() {
2704            return Ok(InputStateBatchCasOutcome::Swapped);
2705        }
2706
2707        #[cfg(test)]
2708        let before_block = {
2709            self.input_state_batch_cas_before
2710                .lock()
2711                .unwrap_or_else(std::sync::PoisonError::into_inner)
2712                .take()
2713        };
2714        #[cfg(test)]
2715        if let Some((entered, release)) = before_block {
2716            entered.notify_one();
2717            release.notified().await;
2718        }
2719
2720        let mut inner = self.inner.lock().await;
2721        let Some(states) = inner.input_states.get_mut(&runtime_id.0) else {
2722            return Ok(InputStateBatchCasOutcome::Stale);
2723        };
2724        let mut all_expected = true;
2725        let mut all_replacements = true;
2726        for row in &prepared {
2727            let Some(current) = states.get(&row.input_id) else {
2728                return Ok(InputStateBatchCasOutcome::Stale);
2729            };
2730            let current_json = serde_json::to_vec(current)
2731                .map_err(|error| RuntimeStoreError::ReadFailed(error.to_string()))?;
2732            if current_json != row.expected_json {
2733                all_expected = false;
2734            }
2735            if current_json != row.replacement_json {
2736                all_replacements = false;
2737            }
2738        }
2739        if all_replacements {
2740            return Ok(InputStateBatchCasOutcome::Swapped);
2741        }
2742        if !all_expected {
2743            return Ok(InputStateBatchCasOutcome::Stale);
2744        }
2745        let _ = states;
2746        let prepared = prepare_memory_input_state_mutations(
2747            &inner,
2748            &runtime_id.0,
2749            prepared
2750                .into_iter()
2751                .map(|row| MemoryInputStateMutation::Upsert(row.replacement))
2752                .collect(),
2753        )?;
2754        apply_prepared_memory_input_state_mutations(&mut inner, &runtime_id.0, prepared);
2755        drop(inner);
2756
2757        #[cfg(test)]
2758        let after_commit_block = {
2759            self.input_state_batch_cas_after_commit
2760                .lock()
2761                .unwrap_or_else(std::sync::PoisonError::into_inner)
2762                .take()
2763        };
2764        #[cfg(test)]
2765        if let Some((entered, release)) = after_commit_block {
2766            entered.notify_one();
2767            release.notified().await;
2768        }
2769        Ok(InputStateBatchCasOutcome::Swapped)
2770    }
2771
2772    async fn compare_and_swap_input_states_atomically_with_fence(
2773        &self,
2774        runtime_id: &LogicalRuntimeId,
2775        expected: &[StoredInputState],
2776        replacements: &[InputStatePersistenceRecord],
2777        write_fence: Arc<dyn RuntimeStoreWriteFence>,
2778    ) -> Result<FencedInputStateBatchCasOutcome, RuntimeStoreError> {
2779        let prepared = prepare_input_state_batch_cas(expected, replacements)?;
2780        if prepared.is_empty() {
2781            return Ok(FencedInputStateBatchCasOutcome::Swapped);
2782        }
2783
2784        let mut inner = self.inner.lock().await;
2785        let Some(states) = inner.input_states.get_mut(&runtime_id.0) else {
2786            return Ok(FencedInputStateBatchCasOutcome::Stale);
2787        };
2788        let mut all_expected = true;
2789        let mut all_replacements = true;
2790        for row in &prepared {
2791            let Some(current) = states.get(&row.input_id) else {
2792                return Ok(FencedInputStateBatchCasOutcome::Stale);
2793            };
2794            let current_json = serde_json::to_vec(current)
2795                .map_err(|error| RuntimeStoreError::ReadFailed(error.to_string()))?;
2796            if current_json != row.expected_json {
2797                all_expected = false;
2798            }
2799            if current_json != row.replacement_json {
2800                all_replacements = false;
2801            }
2802        }
2803        if !all_replacements && !all_expected {
2804            return Ok(FencedInputStateBatchCasOutcome::Stale);
2805        }
2806
2807        let _ = states;
2808        let prepared = if all_replacements {
2809            Vec::new()
2810        } else {
2811            prepare_memory_input_state_mutations(
2812                &inner,
2813                &runtime_id.0,
2814                prepared
2815                    .into_iter()
2816                    .map(|row| MemoryInputStateMutation::Upsert(row.replacement))
2817                    .collect(),
2818            )?
2819        };
2820        let fence_outcome = execute_runtime_store_write_fence(write_fence.as_ref(), || {
2821            apply_prepared_memory_input_state_mutations(&mut inner, &runtime_id.0, prepared);
2822            Ok(())
2823        })?;
2824        match fence_outcome {
2825            RuntimeStoreWriteFenceOutcome::Applied => Ok(FencedInputStateBatchCasOutcome::Swapped),
2826            RuntimeStoreWriteFenceOutcome::Conflict { reason } => {
2827                Ok(FencedInputStateBatchCasOutcome::FenceConflict { reason })
2828            }
2829            RuntimeStoreWriteFenceOutcome::Backoff { reason } => {
2830                Ok(FencedInputStateBatchCasOutcome::FenceBackoff { reason })
2831            }
2832        }
2833    }
2834
2835    async fn compare_and_swap_recovery_input_states_atomically(
2836        &self,
2837        runtime_id: &LogicalRuntimeId,
2838        expected_revision: RecoveryInputSetRevision,
2839        mutations: &[RecoveryInputStateMutation],
2840    ) -> Result<InputStateBatchCasOutcome, RuntimeStoreError> {
2841        let prepared = prepare_recovery_input_state_mutations(mutations)?;
2842        let mut inner = self.inner.lock().await;
2843        let current_revision = inner
2844            .recovery_input_set_revisions
2845            .get(&runtime_id.0)
2846            .copied()
2847            .unwrap_or(0);
2848        if current_revision != expected_revision.store_generation() {
2849            return Ok(InputStateBatchCasOutcome::Stale);
2850        }
2851
2852        let states = inner.input_states.get(&runtime_id.0);
2853        let mut changed = Vec::new();
2854        for mutation in prepared {
2855            let Some(current) = states.and_then(|states| states.get(mutation.input_id())) else {
2856                return Ok(InputStateBatchCasOutcome::Stale);
2857            };
2858            if memory_input_row_version_digest(current)? != mutation.expected_row_digest() {
2859                return Ok(InputStateBatchCasOutcome::Stale);
2860            }
2861            match &mutation {
2862                PreparedRecoveryInputStateMutation::Upsert { replacement, .. } => {
2863                    let current_bytes = serde_json::to_vec(current)
2864                        .map_err(|error| RuntimeStoreError::ReadFailed(error.to_string()))?;
2865                    let replacement_bytes = serde_json::to_vec(replacement)
2866                        .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
2867                    if current_bytes != replacement_bytes {
2868                        changed.push(mutation);
2869                    }
2870                }
2871                PreparedRecoveryInputStateMutation::Delete { .. } => changed.push(mutation),
2872            }
2873        }
2874        let prepared = prepare_memory_input_state_mutations(
2875            &inner,
2876            &runtime_id.0,
2877            changed
2878                .into_iter()
2879                .map(|mutation| match mutation {
2880                    PreparedRecoveryInputStateMutation::Upsert { replacement, .. } => {
2881                        MemoryInputStateMutation::Upsert(replacement)
2882                    }
2883                    PreparedRecoveryInputStateMutation::Delete { input_id, .. } => {
2884                        MemoryInputStateMutation::Delete(input_id)
2885                    }
2886                })
2887                .collect(),
2888        )?;
2889        apply_prepared_memory_input_state_mutations(&mut inner, &runtime_id.0, prepared);
2890        Ok(InputStateBatchCasOutcome::Swapped)
2891    }
2892
2893    async fn compare_and_swap_recovery_input_states_atomically_with_fence(
2894        &self,
2895        runtime_id: &LogicalRuntimeId,
2896        expected_revision: RecoveryInputSetRevision,
2897        mutations: &[RecoveryInputStateMutation],
2898        write_fence: Arc<dyn RuntimeStoreWriteFence>,
2899    ) -> Result<FencedInputStateBatchCasOutcome, RuntimeStoreError> {
2900        let prepared = prepare_recovery_input_state_mutations(mutations)?;
2901        let mut inner = self.inner.lock().await;
2902        let current_revision = inner
2903            .recovery_input_set_revisions
2904            .get(&runtime_id.0)
2905            .copied()
2906            .unwrap_or(0);
2907        if current_revision != expected_revision.store_generation() {
2908            return Ok(FencedInputStateBatchCasOutcome::Stale);
2909        }
2910
2911        let states = inner.input_states.get(&runtime_id.0);
2912        let mut changed = Vec::new();
2913        for mutation in prepared {
2914            let Some(current) = states.and_then(|states| states.get(mutation.input_id())) else {
2915                return Ok(FencedInputStateBatchCasOutcome::Stale);
2916            };
2917            if memory_input_row_version_digest(current)? != mutation.expected_row_digest() {
2918                return Ok(FencedInputStateBatchCasOutcome::Stale);
2919            }
2920            match &mutation {
2921                PreparedRecoveryInputStateMutation::Upsert { replacement, .. } => {
2922                    let current_bytes = serde_json::to_vec(current)
2923                        .map_err(|error| RuntimeStoreError::ReadFailed(error.to_string()))?;
2924                    let replacement_bytes = serde_json::to_vec(replacement)
2925                        .map_err(|error| RuntimeStoreError::WriteFailed(error.to_string()))?;
2926                    if current_bytes != replacement_bytes {
2927                        changed.push(mutation);
2928                    }
2929                }
2930                PreparedRecoveryInputStateMutation::Delete { .. } => changed.push(mutation),
2931            }
2932        }
2933        let prepared = prepare_memory_input_state_mutations(
2934            &inner,
2935            &runtime_id.0,
2936            changed
2937                .into_iter()
2938                .map(|mutation| match mutation {
2939                    PreparedRecoveryInputStateMutation::Upsert { replacement, .. } => {
2940                        MemoryInputStateMutation::Upsert(replacement)
2941                    }
2942                    PreparedRecoveryInputStateMutation::Delete { input_id, .. } => {
2943                        MemoryInputStateMutation::Delete(input_id)
2944                    }
2945                })
2946                .collect(),
2947        )?;
2948
2949        let fence_outcome = execute_runtime_store_write_fence(write_fence.as_ref(), || {
2950            apply_prepared_memory_input_state_mutations(&mut inner, &runtime_id.0, prepared);
2951            Ok(())
2952        })?;
2953        match fence_outcome {
2954            RuntimeStoreWriteFenceOutcome::Applied => Ok(FencedInputStateBatchCasOutcome::Swapped),
2955            RuntimeStoreWriteFenceOutcome::Conflict { reason } => {
2956                Ok(FencedInputStateBatchCasOutcome::FenceConflict { reason })
2957            }
2958            RuntimeStoreWriteFenceOutcome::Backoff { reason } => {
2959                Ok(FencedInputStateBatchCasOutcome::FenceBackoff { reason })
2960            }
2961        }
2962    }
2963
2964    async fn load_input_state(
2965        &self,
2966        runtime_id: &LogicalRuntimeId,
2967        input_id: &InputId,
2968    ) -> Result<Option<StoredInputState>, RuntimeStoreError> {
2969        let inner = self.inner.lock().await;
2970        let state = inner
2971            .input_states
2972            .get(&runtime_id.0)
2973            .and_then(|m| m.get(input_id).cloned());
2974        Ok(state)
2975    }
2976
2977    async fn load_input_state_by_idempotency_key(
2978        &self,
2979        runtime_id: &LogicalRuntimeId,
2980        key: &IdempotencyKey,
2981    ) -> Result<Option<ExactInputStateObservation>, RuntimeStoreError> {
2982        let inner = self.inner.lock().await;
2983        let uncertain = |evidence_input_id: String, reason: String| {
2984            RuntimeStoreError::InputIdempotencyIndexUncertain {
2985                runtime_id: runtime_id.to_string(),
2986                key: key.to_string(),
2987                evidence_input_id,
2988                reason,
2989            }
2990        };
2991        let Some(input_id) = inner
2992            .input_idempotency_index
2993            .get(&runtime_id.0)
2994            .and_then(|index| index.get(&key.0))
2995        else {
2996            return Ok(None);
2997        };
2998        let state = inner
2999            .input_states
3000            .get(&runtime_id.0)
3001            .and_then(|states| states.get(input_id))
3002            .cloned()
3003            .ok_or_else(|| {
3004                uncertain(
3005                    input_id.to_string(),
3006                    "index names a missing source input row".to_string(),
3007                )
3008            })?;
3009        if &state.state.input_id != input_id || state.state.idempotency_key.as_ref() != Some(key) {
3010            return Err(uncertain(
3011                input_id.to_string(),
3012                format!(
3013                    "index owner differs from source identity/key (decoded input {}, decoded key \
3014                     {:?})",
3015                    state.state.input_id, state.state.idempotency_key
3016                ),
3017            ));
3018        }
3019        crate::meerkat_machine::authorize_stored_input_state_seed(
3020            &state.state.input_id,
3021            &state.seed,
3022        )
3023        .map_err(|error| {
3024            uncertain(
3025                input_id.to_string(),
3026                format!("indexed source input row has a non-authoritative machine seed: {error}"),
3027            )
3028        })?;
3029        let exact_row_digest = memory_input_row_version_digest(&state).map_err(|error| {
3030            uncertain(
3031                input_id.to_string(),
3032                format!("indexed source input row could not be encoded exactly: {error}"),
3033            )
3034        })?;
3035        ExactInputStateObservation::from_exact_stored_row(state, exact_row_digest)
3036            .map(Some)
3037            .map_err(|error| {
3038                uncertain(
3039                    input_id.to_string(),
3040                    format!("indexed source row could not produce an exact observation: {error}"),
3041                )
3042            })
3043    }
3044
3045    async fn load_input_states_by_ids(
3046        &self,
3047        runtime_id: &LogicalRuntimeId,
3048        input_ids: &[InputId],
3049    ) -> Result<Vec<Option<StoredInputState>>, RuntimeStoreError> {
3050        validate_input_state_batch_read_ids(input_ids)?;
3051        let inner = self.inner.lock().await;
3052        let states = inner.input_states.get(&runtime_id.0);
3053        Ok(input_ids
3054            .iter()
3055            .map(|input_id| states.and_then(|rows| rows.get(input_id).cloned()))
3056            .collect())
3057    }
3058
3059    async fn load_pending_terminal_owner_ids_page(
3060        &self,
3061        runtime_id: &LogicalRuntimeId,
3062        after: Option<&InputId>,
3063        limit: usize,
3064    ) -> Result<Vec<InputId>, RuntimeStoreError> {
3065        super::validate_pending_terminal_owner_page(after, limit, &[])?;
3066        let inner = self.inner.lock().await;
3067        let Some(owners) = inner.pending_terminal_owners.get(&runtime_id.0) else {
3068            return Ok(Vec::new());
3069        };
3070        let lower = after
3071            .map(|input_id| std::ops::Bound::Excluded(input_id.0))
3072            .unwrap_or(std::ops::Bound::Unbounded);
3073        let owner_input_ids = owners
3074            .range((lower, std::ops::Bound::Unbounded))
3075            .take(limit)
3076            .copied()
3077            .map(InputId::from_uuid)
3078            .collect::<Vec<_>>();
3079        super::validate_pending_terminal_owner_page(after, limit, &owner_input_ids)?;
3080        Ok(owner_input_ids)
3081    }
3082
3083    async fn observe_machine_lifecycle(
3084        &self,
3085        runtime_id: &LogicalRuntimeId,
3086    ) -> Result<MachineLifecycleObservation, RuntimeStoreError> {
3087        #[cfg(test)]
3088        if self
3089            .machine_lifecycle_observe_errors_remaining
3090            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |remaining| {
3091                remaining.checked_sub(1)
3092            })
3093            .is_ok()
3094        {
3095            return Err(RuntimeStoreError::ReadFailed(
3096                "synthetic machine lifecycle transport failure".to_string(),
3097            ));
3098        }
3099        let inner = self.inner.lock().await;
3100        Ok(inner
3101            .runtime_lifecycle
3102            .get(&runtime_id.0)
3103            .map_or(MachineLifecycleObservation::Missing, |bytes| {
3104                classify_machine_lifecycle_record(bytes)
3105            }))
3106    }
3107
3108    async fn compare_and_swap_machine_lifecycle(
3109        &self,
3110        runtime_id: &LogicalRuntimeId,
3111        expected: MachineLifecycleExpectedVersion,
3112        replacement: MachineLifecycleCommit,
3113    ) -> Result<MachineLifecycleCasOutcome, RuntimeStoreError> {
3114        let replacement = prepare_machine_lifecycle_replacement(replacement)?;
3115        let mut inner = self.inner.lock().await;
3116        let current_raw = inner.runtime_lifecycle.get(&runtime_id.0).cloned();
3117        let current = current_raw.as_deref().map_or(
3118            MachineLifecycleObservation::Missing,
3119            classify_machine_lifecycle_record,
3120        );
3121        #[cfg(test)]
3122        if self
3123            .machine_lifecycle_cas_conflicts_remaining
3124            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |remaining| {
3125                remaining.checked_sub(1)
3126            })
3127            .is_ok()
3128        {
3129            return Ok(MachineLifecycleCasOutcome::Conflict { current });
3130        }
3131        let matches = match (&expected, &current) {
3132            (MachineLifecycleExpectedVersion::Missing, MachineLifecycleObservation::Missing) => {
3133                true
3134            }
3135            (MachineLifecycleExpectedVersion::Version(expected), current) => {
3136                current.version().is_some_and(|actual| actual == expected)
3137            }
3138            _ => false,
3139        };
3140        if !matches {
3141            return Ok(MachineLifecycleCasOutcome::Conflict { current });
3142        }
3143        let replacement = replacement.preserve_observed_custody(&current)?;
3144        validate_machine_lifecycle_replacement(
3145            &current,
3146            current_raw.as_deref(),
3147            &replacement.snapshot,
3148        )?;
3149        let runtime_state = replacement.snapshot.runtime_state();
3150        inner
3151            .runtime_lifecycle
3152            .insert(runtime_id.0.clone(), replacement.bytes);
3153        sync_runtime_session_catalog_lifecycle(&mut inner, &runtime_id.0, runtime_state);
3154        Ok(MachineLifecycleCasOutcome::Applied {
3155            version: replacement.version,
3156        })
3157    }
3158
3159    async fn compare_and_swap_machine_lifecycle_with_fence(
3160        &self,
3161        runtime_id: &LogicalRuntimeId,
3162        expected: MachineLifecycleExpectedVersion,
3163        replacement: MachineLifecycleCommit,
3164        write_fence: Arc<dyn RuntimeStoreWriteFence>,
3165    ) -> Result<FencedMachineLifecycleCasOutcome, RuntimeStoreError> {
3166        let replacement = prepare_machine_lifecycle_replacement(replacement)?;
3167        let mut inner = self.inner.lock().await;
3168        let current_raw = inner.runtime_lifecycle.get(&runtime_id.0).cloned();
3169        let current = current_raw.as_deref().map_or(
3170            MachineLifecycleObservation::Missing,
3171            classify_machine_lifecycle_record,
3172        );
3173        let matches = match (&expected, &current) {
3174            (MachineLifecycleExpectedVersion::Missing, MachineLifecycleObservation::Missing) => {
3175                true
3176            }
3177            (MachineLifecycleExpectedVersion::Version(expected), current) => {
3178                current.version().is_some_and(|actual| actual == expected)
3179            }
3180            _ => false,
3181        };
3182        if !matches {
3183            return Ok(FencedMachineLifecycleCasOutcome::Conflict { current });
3184        }
3185        let replacement = replacement.preserve_observed_custody(&current)?;
3186        validate_machine_lifecycle_replacement(
3187            &current,
3188            current_raw.as_deref(),
3189            &replacement.snapshot,
3190        )?;
3191        let already_exact = current_raw.as_deref() == Some(replacement.bytes.as_slice());
3192        let record = decoded_prepared_machine_lifecycle_replacement(&replacement)?;
3193        let version = replacement.version.clone();
3194        let runtime_state = replacement.snapshot.runtime_state();
3195        let fence_outcome = execute_runtime_store_write_fence(write_fence.as_ref(), || {
3196            if !already_exact {
3197                inner
3198                    .runtime_lifecycle
3199                    .insert(runtime_id.0.clone(), replacement.bytes.clone());
3200            }
3201            sync_runtime_session_catalog_lifecycle(&mut inner, &runtime_id.0, runtime_state);
3202            Ok(())
3203        })?;
3204        match fence_outcome {
3205            RuntimeStoreWriteFenceOutcome::Applied if already_exact => {
3206                Ok(FencedMachineLifecycleCasOutcome::AlreadyExact { record, version })
3207            }
3208            RuntimeStoreWriteFenceOutcome::Applied => {
3209                Ok(FencedMachineLifecycleCasOutcome::Applied { record, version })
3210            }
3211            RuntimeStoreWriteFenceOutcome::Conflict { reason } => {
3212                Ok(FencedMachineLifecycleCasOutcome::FenceConflict { reason })
3213            }
3214            RuntimeStoreWriteFenceOutcome::Backoff { reason } => {
3215                Ok(FencedMachineLifecycleCasOutcome::FenceBackoff { reason })
3216            }
3217        }
3218    }
3219
3220    async fn load_machine_lifecycle_record(
3221        &self,
3222        runtime_id: &LogicalRuntimeId,
3223    ) -> Result<Option<Vec<u8>>, RuntimeStoreError> {
3224        let inner = self.inner.lock().await;
3225        Ok(inner.runtime_lifecycle.get(&runtime_id.0).cloned())
3226    }
3227
3228    async fn commit_machine_lifecycle(
3229        &self,
3230        runtime_id: &LogicalRuntimeId,
3231        commit: MachineLifecycleCommit,
3232        input_states: &[InputStatePersistenceRecord],
3233    ) -> Result<(), RuntimeStoreError> {
3234        let runtime_state = commit.runtime_state();
3235        let record = commit.store_record().encode()?;
3236        let mut inner = self.inner.lock().await;
3237        let rid = runtime_id.0.clone();
3238        let prepared_input_mutations = prepare_memory_input_state_mutations(
3239            &inner,
3240            &rid,
3241            input_states
3242                .iter()
3243                .map(|record| MemoryInputStateMutation::Upsert(record.clone_stored()))
3244                .collect(),
3245        )?;
3246
3247        // Single lock acquisition — atomic for in-memory
3248        inner.runtime_lifecycle.insert(rid.clone(), record);
3249        sync_runtime_session_catalog_lifecycle(&mut inner, &rid, runtime_state);
3250        apply_prepared_memory_input_state_mutations(&mut inner, &rid, prepared_input_mutations);
3251
3252        Ok(())
3253    }
3254
3255    async fn commit_unregister_finalization(
3256        &self,
3257        runtime_id: &LogicalRuntimeId,
3258        finalization: crate::store::UnregisterFinalizationCommit,
3259    ) -> Result<(), RuntimeStoreError> {
3260        let (snapshot, input_states, retired_ops_epoch) = finalization.into_parts();
3261        let runtime_state = snapshot.runtime_state();
3262        let lifecycle_record = MachineLifecycleStoreRecord::from_snapshot(&snapshot).encode()?;
3263        let mut inner = self.inner.lock().await;
3264        let rid = runtime_id.0.clone();
3265        let prepared_input_mutations = prepare_memory_input_state_mutations(
3266            &inner,
3267            &rid,
3268            input_states
3269                .into_iter()
3270                .map(|record| MemoryInputStateMutation::Upsert(record.clone_stored()))
3271                .collect(),
3272        )?;
3273
3274        // One lock acquisition is the in-memory transaction boundary. The
3275        // finalization token prepared every owned value before this method, so
3276        // no fallible request preparation remains after the first mutation.
3277        inner
3278            .runtime_lifecycle
3279            .insert(rid.clone(), lifecycle_record);
3280        sync_runtime_session_catalog_lifecycle(&mut inner, &rid, runtime_state);
3281        apply_prepared_memory_input_state_mutations(&mut inner, &rid, prepared_input_mutations);
3282        if inner
3283            .ops_lifecycle_snapshots
3284            .get(&rid)
3285            .is_some_and(|snapshot| snapshot.epoch_id == retired_ops_epoch)
3286        {
3287            inner.ops_lifecycle_snapshots.remove(&rid);
3288        }
3289        inner.retired_ops_epochs.insert((rid, retired_ops_epoch));
3290        Ok(())
3291    }
3292
3293    async fn persist_ops_lifecycle(
3294        &self,
3295        runtime_id: &LogicalRuntimeId,
3296        snapshot: &PersistedOpsSnapshot,
3297    ) -> Result<(), RuntimeStoreError> {
3298        let mut inner = self.inner.lock().await;
3299        if inner
3300            .retired_ops_epochs
3301            .contains(&(runtime_id.0.clone(), snapshot.epoch_id.clone()))
3302        {
3303            return Err(RuntimeStoreError::OpsLifecycleEpochRetired {
3304                runtime_id: runtime_id.0.clone(),
3305                epoch_id: snapshot.epoch_id.clone(),
3306            });
3307        }
3308        inner
3309            .ops_lifecycle_snapshots
3310            .insert(runtime_id.0.clone(), snapshot.clone());
3311        Ok(())
3312    }
3313
3314    async fn initialize_ops_lifecycle_if_absent(
3315        &self,
3316        runtime_id: &LogicalRuntimeId,
3317        candidate: &PersistedOpsSnapshot,
3318    ) -> Result<PersistedOpsSnapshot, RuntimeStoreError> {
3319        let mut inner = self.inner.lock().await;
3320        let key = runtime_id.0.clone();
3321        if inner
3322            .retired_ops_epochs
3323            .contains(&(key.clone(), candidate.epoch_id.clone()))
3324        {
3325            return Err(RuntimeStoreError::OpsLifecycleEpochRetired {
3326                runtime_id: key,
3327                epoch_id: candidate.epoch_id.clone(),
3328            });
3329        }
3330        let canonical = inner
3331            .ops_lifecycle_snapshots
3332            .entry(key)
3333            .or_insert_with(|| candidate.clone())
3334            .clone();
3335        if inner
3336            .retired_ops_epochs
3337            .contains(&(runtime_id.0.clone(), canonical.epoch_id.clone()))
3338        {
3339            return Err(RuntimeStoreError::OpsLifecycleEpochRetired {
3340                runtime_id: runtime_id.0.clone(),
3341                epoch_id: canonical.epoch_id,
3342            });
3343        }
3344        Ok(canonical)
3345    }
3346
3347    async fn load_ops_lifecycle(
3348        &self,
3349        runtime_id: &LogicalRuntimeId,
3350    ) -> Result<Option<PersistedOpsSnapshot>, RuntimeStoreError> {
3351        let inner = self.inner.lock().await;
3352        Ok(inner.ops_lifecycle_snapshots.get(&runtime_id.0).cloned())
3353    }
3354
3355    async fn delete_ops_lifecycle(
3356        &self,
3357        runtime_id: &LogicalRuntimeId,
3358    ) -> Result<(), RuntimeStoreError> {
3359        let mut inner = self.inner.lock().await;
3360        inner.ops_lifecycle_snapshots.remove(&runtime_id.0);
3361        Ok(())
3362    }
3363}
3364
3365#[cfg(test)]
3366#[allow(clippy::unwrap_used)]
3367mod tests {
3368    use super::*;
3369    use crate::RuntimeState;
3370    use crate::store::MachineLifecycleBindingFacts;
3371    use meerkat_core::lifecycle::run_primitive::RunApplyBoundary;
3372
3373    #[tokio::test]
3374    async fn pending_terminal_owner_index_satisfies_store_contract() {
3375        crate::store::assert_pending_terminal_owner_index_contract(&InMemoryRuntimeStore::new())
3376            .await;
3377    }
3378
3379    fn make_receipt(run_id: RunId, seq: u64) -> RunBoundaryReceipt {
3380        RunBoundaryReceipt {
3381            run_id,
3382            boundary: RunApplyBoundary::RunStart,
3383            contributing_input_ids: vec![],
3384            conversation_digest: None,
3385            message_count: 0,
3386            sequence: seq,
3387        }
3388    }
3389
3390    fn lifecycle_commit(
3391        runtime_id: &LogicalRuntimeId,
3392        state: RuntimeState,
3393        fence_token: u64,
3394        runtime_generation: u64,
3395    ) -> MachineLifecycleCommit {
3396        MachineLifecycleCommit::new_with_binding(
3397            state,
3398            MachineLifecycleBindingFacts::new(
3399                Some(runtime_id.0.clone()),
3400                Some(fence_token),
3401                Some(runtime_generation),
3402                Some(format!("epoch-{runtime_generation}")),
3403            ),
3404            crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
3405        )
3406    }
3407
3408    struct AppliedWriteFence;
3409
3410    impl RuntimeStoreWriteFence for AppliedWriteFence {
3411        fn execute_if_current(
3412            &self,
3413            operation: Box<dyn FnOnce() -> Result<(), RuntimeStoreError> + '_>,
3414        ) -> Result<RuntimeStoreWriteFenceOutcome, RuntimeStoreError> {
3415            operation()?;
3416            Ok(RuntimeStoreWriteFenceOutcome::Applied)
3417        }
3418    }
3419
3420    fn persistable(bundle: StoredInputState) -> InputStatePersistenceRecord {
3421        InputStatePersistenceRecord::from_machine_snapshot(bundle).unwrap()
3422    }
3423
3424    fn session_with_user(content: &str) -> meerkat_core::Session {
3425        let mut session = meerkat_core::Session::new();
3426        session.push(meerkat_core::types::Message::User(
3427            meerkat_core::types::UserMessage::text(content.to_string()),
3428        ));
3429        session
3430    }
3431
3432    fn encode_as_released_0810_compaction_fixture(
3433        session: &meerkat_core::Session,
3434    ) -> serde_json::Value {
3435        let history = session
3436            .validated_transcript_history_state()
3437            .unwrap()
3438            .expect("fixture rewrite graph exists");
3439        assert_eq!(
3440            history.digest_format(),
3441            3,
3442            "fixture source must exercise the current transcript format"
3443        );
3444        assert_eq!(history.commit_count(), 1, "fixture has one rewrite");
3445        let commit = history.last_commit().expect("fixture rewrite commit");
3446        let (start, end) = commit.selection.bounds();
3447        let mut released_commit = serde_json::to_value(commit).unwrap();
3448        released_commit
3449            .as_object_mut()
3450            .unwrap()
3451            .remove("rewrite_generation");
3452        released_commit["selection"] = serde_json::json!({
3453            "type": "compaction_message_range",
3454            "range": { "start": start, "end": end }
3455        });
3456        let released_graph = serde_json::json!({
3457            "head": history.head(),
3458            "commits": [released_commit],
3459            "revisions": [
3460                history.materialize_revision(&commit.parent_revision).unwrap(),
3461                history.materialize_revision(&commit.revision).unwrap(),
3462            ],
3463            "digest_format": 2,
3464        });
3465        let mut encoded = serde_json::to_value(session).unwrap();
3466        encoded["version"] = serde_json::json!(2);
3467        encoded["metadata"][meerkat_core::SESSION_TRANSCRIPT_HISTORY_STATE_KEY] = released_graph;
3468        encoded["metadata"]
3469            .as_object_mut()
3470            .unwrap()
3471            .remove(meerkat_core::SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY);
3472        encoded
3473    }
3474
3475    fn compaction_commit_fingerprint(commit: &meerkat_core::TranscriptRewriteCommit) -> String {
3476        use sha2::{Digest as _, Sha256};
3477
3478        #[derive(serde::Serialize)]
3479        struct Fingerprint<'a> {
3480            selection: &'a meerkat_core::TranscriptRewriteSelection,
3481            original_span_digest: &'a str,
3482            replacement_digest: &'a str,
3483            messages_before: usize,
3484            messages_after: usize,
3485            actor: &'a Option<String>,
3486        }
3487
3488        let canonical = serde_json::to_vec(&Fingerprint {
3489            selection: &commit.selection,
3490            original_span_digest: &commit.original_span_digest,
3491            replacement_digest: &commit.replacement_digest,
3492            messages_before: commit.messages_before,
3493            messages_after: commit.messages_after,
3494            actor: &commit.actor,
3495        })
3496        .unwrap();
3497        format!("sha256:{:x}", Sha256::digest(canonical))
3498    }
3499
3500    fn session_with_compaction_intent() -> (
3501        meerkat_core::Session,
3502        meerkat_core::CompactionProjectionIntent,
3503    ) {
3504        let mut session = session_with_user("verbose context one");
3505        session.push(meerkat_core::types::Message::User(
3506            meerkat_core::types::UserMessage::text("verbose context two"),
3507        ));
3508        let parent = session.transcript_revision().unwrap();
3509        session
3510            .commit_transcript_rewrite(
3511                meerkat_core::TranscriptRewriteSelection::MessageRange { start: 0, end: 2 },
3512                vec![meerkat_core::types::Message::User(
3513                    meerkat_core::types::UserMessage::compaction_summary("compacted context"),
3514                )],
3515                meerkat_core::TranscriptRewriteReason::new("compaction"),
3516                Some("runtime-store-test".to_string()),
3517                Some(parent),
3518            )
3519            .unwrap();
3520        let encoded = encode_as_released_0810_compaction_fixture(&session);
3521        let encoded = serde_json::to_vec(&encoded).unwrap();
3522        let (mut session, _import_receipt) = meerkat_core::import_released_0810_session(&encoded)
3523            .unwrap()
3524            .into_parts();
3525        let commit = session
3526            .validated_transcript_history_state()
3527            .unwrap()
3528            .unwrap()
3529            .last_commit()
3530            .unwrap()
3531            .clone();
3532        let commit_fingerprint = compaction_commit_fingerprint(&commit);
3533        let intent = meerkat_core::CompactionProjectionIntent {
3534            projection: serde_json::from_value(serde_json::json!({
3535                "session_id": session.id(),
3536                "parent_revision": &commit.parent_revision,
3537                "revision": &commit.revision,
3538                "commit_fingerprint": commit_fingerprint,
3539            }))
3540            .unwrap(),
3541            summary_tokens: 5,
3542            messages_before: 2,
3543            messages_after: 1,
3544        };
3545        session
3546            .add_compaction_projection_intent(intent.clone())
3547            .unwrap();
3548        (session, intent)
3549    }
3550
3551    fn snapshot_with_raw_intents(
3552        session: &meerkat_core::Session,
3553        intents: &[meerkat_core::CompactionProjectionIntent],
3554    ) -> Vec<u8> {
3555        let mut value = serde_json::to_value(session).unwrap();
3556        value["metadata"][meerkat_core::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY] =
3557            serde_json::to_value(intents).unwrap();
3558        serde_json::to_vec(&value).unwrap()
3559    }
3560
3561    fn unbacked_intent(
3562        session_id: &meerkat_core::types::SessionId,
3563    ) -> meerkat_core::CompactionProjectionIntent {
3564        meerkat_core::CompactionProjectionIntent {
3565            projection: serde_json::from_value(serde_json::json!({
3566                "session_id": session_id,
3567                "parent_revision": "missing-parent",
3568                "revision": "missing-revision",
3569                "commit_fingerprint": "sha256:unbacked-persisted-fixture",
3570            }))
3571            .unwrap(),
3572            summary_tokens: 1,
3573            messages_before: 2,
3574            messages_after: 1,
3575        }
3576    }
3577
3578    #[tokio::test]
3579    async fn atomic_apply_commits_rewrite_and_compaction_outbox_as_one_boundary() {
3580        let store = InMemoryRuntimeStore::new();
3581        let (session, intent) = session_with_compaction_intent();
3582        let rid = LogicalRuntimeId::for_session(session.id());
3583        let snapshot = serde_json::to_vec(&session).unwrap();
3584        store
3585            .atomic_apply(
3586                &rid,
3587                Some(SerializedSessionSnapshot {
3588                    session_snapshot: snapshot.clone().into(),
3589                }),
3590                make_receipt(RunId::new(), 1),
3591                vec![],
3592                Some(session.id().clone()),
3593            )
3594            .await
3595            .unwrap();
3596        assert_eq!(
3597            store.load_session_snapshot(&rid).await.unwrap(),
3598            Some(Arc::new(snapshot))
3599        );
3600        assert_eq!(
3601            store
3602                .load_pending_compaction_projections(&rid)
3603                .await
3604                .unwrap(),
3605            vec![intent.clone()]
3606        );
3607        store
3608            .mark_compaction_projection_finalized(&rid, &intent.projection)
3609            .await
3610            .unwrap();
3611        store
3612            .mark_compaction_projection_finalized(&rid, &intent.projection)
3613            .await
3614            .unwrap();
3615        assert!(
3616            store
3617                .load_pending_compaction_projections(&rid)
3618                .await
3619                .unwrap()
3620                .is_empty()
3621        );
3622        let persisted: meerkat_core::Session =
3623            serde_json::from_slice(&store.load_session_snapshot(&rid).await.unwrap().unwrap())
3624                .unwrap();
3625        assert!(
3626            persisted
3627                .compaction_projection_intents()
3628                .unwrap()
3629                .is_empty()
3630        );
3631    }
3632
3633    #[tokio::test]
3634    async fn finalized_outbox_tombstone_rejects_atomic_and_non_boundary_snapshot_replay() {
3635        let store = InMemoryRuntimeStore::new();
3636        let (session, intent) = session_with_compaction_intent();
3637        let rid = LogicalRuntimeId::for_session(session.id());
3638        let replay_snapshot = serde_json::to_vec(&session).unwrap();
3639        store
3640            .atomic_apply(
3641                &rid,
3642                Some(SerializedSessionSnapshot {
3643                    session_snapshot: replay_snapshot.clone().into(),
3644                }),
3645                make_receipt(RunId::new(), 1),
3646                vec![],
3647                Some(session.id().clone()),
3648            )
3649            .await
3650            .unwrap();
3651        store
3652            .mark_compaction_projection_finalized(&rid, &intent.projection)
3653            .await
3654            .unwrap();
3655        let cleaned_snapshot = store.load_session_snapshot(&rid).await.unwrap().unwrap();
3656
3657        let replay_run_id = RunId::new();
3658        let error = store
3659            .atomic_apply(
3660                &rid,
3661                Some(SerializedSessionSnapshot {
3662                    session_snapshot: replay_snapshot.clone().into(),
3663                }),
3664                make_receipt(replay_run_id.clone(), 2),
3665                vec![],
3666                Some(session.id().clone()),
3667            )
3668            .await
3669            .unwrap_err();
3670        assert!(error.to_string().contains("finalized compaction intent"));
3671        assert!(
3672            store
3673                .load_boundary_receipt(&rid, &replay_run_id, 2)
3674                .await
3675                .unwrap()
3676                .is_none(),
3677            "finalized replay rejection must roll back the whole atomic boundary"
3678        );
3679
3680        let error = store
3681            .commit_session_snapshot(
3682                &rid,
3683                SerializedSessionSnapshot {
3684                    session_snapshot: replay_snapshot.clone().into(),
3685                },
3686            )
3687            .await
3688            .unwrap_err();
3689        assert!(error.to_string().contains("finalized compaction intent"));
3690        let error = store
3691            .replace_session_snapshot_if_current(&rid, &cleaned_snapshot, replay_snapshot)
3692            .await
3693            .unwrap_err();
3694        assert!(error.to_string().contains("finalized compaction intent"));
3695
3696        assert_eq!(
3697            store.load_session_snapshot(&rid).await.unwrap(),
3698            Some(cleaned_snapshot)
3699        );
3700        assert!(
3701            store
3702                .load_pending_compaction_projections(&rid)
3703                .await
3704                .unwrap()
3705                .is_empty(),
3706            "a finalized tombstone must never be silently revived or left untracked"
3707        );
3708    }
3709
3710    #[tokio::test]
3711    async fn invalid_compaction_intent_leaves_snapshot_and_outbox_unmodified() {
3712        let store = InMemoryRuntimeStore::new();
3713        let (session, mut intent) = session_with_compaction_intent();
3714        let rid = LogicalRuntimeId::for_session(session.id());
3715        intent.summary_tokens += 1;
3716        let conflicting = vec![
3717            session.compaction_projection_intents().unwrap()[0].clone(),
3718            intent,
3719        ];
3720        let error = store
3721            .atomic_apply(
3722                &rid,
3723                Some(SerializedSessionSnapshot {
3724                    session_snapshot: snapshot_with_raw_intents(&session, &conflicting).into(),
3725                }),
3726                make_receipt(RunId::new(), 2),
3727                vec![],
3728                Some(session.id().clone()),
3729            )
3730            .await
3731            .unwrap_err();
3732        assert!(matches!(error, RuntimeStoreError::WriteFailed(_)));
3733        assert_eq!(store.load_session_snapshot(&rid).await.unwrap(), None);
3734        assert!(
3735            store
3736                .load_pending_compaction_projections(&rid)
3737                .await
3738                .unwrap()
3739                .is_empty()
3740        );
3741
3742        let foreign = session_with_compaction_intent().1;
3743        for (sequence, invalid) in [foreign, unbacked_intent(session.id())]
3744            .into_iter()
3745            .enumerate()
3746        {
3747            let error = store
3748                .atomic_apply(
3749                    &rid,
3750                    Some(SerializedSessionSnapshot {
3751                        session_snapshot: snapshot_with_raw_intents(&session, &[invalid]).into(),
3752                    }),
3753                    make_receipt(RunId::new(), 10 + sequence as u64),
3754                    vec![],
3755                    Some(session.id().clone()),
3756                )
3757                .await
3758                .unwrap_err();
3759            assert!(matches!(error, RuntimeStoreError::WriteFailed(_)));
3760            assert_eq!(store.load_session_snapshot(&rid).await.unwrap(), None);
3761            assert!(
3762                store
3763                    .load_pending_compaction_projections(&rid)
3764                    .await
3765                    .unwrap()
3766                    .is_empty()
3767            );
3768        }
3769    }
3770
3771    #[tokio::test]
3772    async fn atomic_apply_commits_compaction_target_state_and_advances_outbox() {
3773        let store = InMemoryRuntimeStore::new();
3774        let (incoming, intent) = session_with_compaction_intent();
3775        let rid = LogicalRuntimeId::for_session(incoming.id());
3776        let mut current = incoming.clone();
3777        current
3778            .complete_compaction_projection_intent(&intent.projection)
3779            .unwrap();
3780        current.push(meerkat_core::types::Message::User(
3781            meerkat_core::types::UserMessage::text("already advanced"),
3782        ));
3783        let current_snapshot = serde_json::to_vec(&current).unwrap();
3784        store
3785            .commit_session_snapshot(
3786                &rid,
3787                SerializedSessionSnapshot {
3788                    session_snapshot: current_snapshot.clone().into(),
3789                },
3790            )
3791            .await
3792            .unwrap();
3793        let incoming_snapshot = serde_json::to_vec(&incoming).unwrap();
3794        let receipt = make_receipt(RunId::new(), 3);
3795        store
3796            .atomic_apply(
3797                &rid,
3798                Some(SerializedSessionSnapshot {
3799                    session_snapshot: incoming_snapshot.clone().into(),
3800                }),
3801                receipt.clone(),
3802                vec![],
3803                Some(incoming.id().clone()),
3804            )
3805            .await
3806            .unwrap();
3807        assert_eq!(
3808            store.load_session_snapshot(&rid).await.unwrap(),
3809            Some(Arc::new(incoming_snapshot))
3810        );
3811        assert_eq!(
3812            store
3813                .load_pending_compaction_projections(&rid)
3814                .await
3815                .unwrap()
3816                .as_slice(),
3817            &[intent]
3818        );
3819        assert_eq!(
3820            store
3821                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
3822                .await
3823                .unwrap(),
3824            Some(receipt)
3825        );
3826    }
3827
3828    #[tokio::test]
3829    async fn existing_outbox_rejects_changed_intent_without_advancing_snapshot() {
3830        let store = InMemoryRuntimeStore::new();
3831        let (session, intent) = session_with_compaction_intent();
3832        let rid = LogicalRuntimeId::for_session(session.id());
3833        let original_snapshot = serde_json::to_vec(&session).unwrap();
3834        store
3835            .atomic_apply(
3836                &rid,
3837                Some(SerializedSessionSnapshot {
3838                    session_snapshot: original_snapshot.clone().into(),
3839                }),
3840                make_receipt(RunId::new(), 60),
3841                vec![],
3842                Some(session.id().clone()),
3843            )
3844            .await
3845            .unwrap();
3846
3847        let mut advanced = session.clone();
3848        advanced.push(meerkat_core::types::Message::User(
3849            meerkat_core::types::UserMessage::text("later turn"),
3850        ));
3851        let mut conflicting = intent.clone();
3852        conflicting.summary_tokens += 1;
3853        let error = store
3854            .atomic_apply(
3855                &rid,
3856                Some(SerializedSessionSnapshot {
3857                    session_snapshot: snapshot_with_raw_intents(&advanced, &[conflicting]).into(),
3858                }),
3859                make_receipt(RunId::new(), 61),
3860                vec![],
3861                Some(session.id().clone()),
3862            )
3863            .await
3864            .unwrap_err();
3865        assert!(matches!(error, RuntimeStoreError::WriteFailed(_)));
3866        assert_eq!(
3867            store.load_session_snapshot(&rid).await.unwrap(),
3868            Some(Arc::new(original_snapshot))
3869        );
3870        assert_eq!(
3871            store
3872                .load_pending_compaction_projections(&rid)
3873                .await
3874                .unwrap(),
3875            vec![intent]
3876        );
3877    }
3878
3879    #[tokio::test]
3880    async fn non_boundary_snapshot_apis_cannot_bypass_compaction_outbox() {
3881        let store = InMemoryRuntimeStore::new();
3882        let (session, _intent) = session_with_compaction_intent();
3883        let rid = LogicalRuntimeId::for_session(session.id());
3884        let snapshot = serde_json::to_vec(&session).unwrap();
3885        assert!(
3886            store
3887                .commit_session_snapshot(
3888                    &rid,
3889                    SerializedSessionSnapshot {
3890                        session_snapshot: snapshot.clone().into(),
3891                    },
3892                )
3893                .await
3894                .is_err()
3895        );
3896        assert_eq!(store.load_session_snapshot(&rid).await.unwrap(), None);
3897        let clean = meerkat_core::Session::with_id(session.id().clone());
3898        let clean_snapshot = serde_json::to_vec(&clean).unwrap();
3899        store
3900            .commit_session_snapshot(
3901                &rid,
3902                SerializedSessionSnapshot {
3903                    session_snapshot: clean_snapshot.clone().into(),
3904                },
3905            )
3906            .await
3907            .unwrap();
3908        assert!(
3909            store
3910                .replace_session_snapshot_if_current(&rid, &clean_snapshot, snapshot)
3911                .await
3912                .is_err()
3913        );
3914        assert_eq!(
3915            store.load_session_snapshot(&rid).await.unwrap(),
3916            Some(Arc::new(clean_snapshot))
3917        );
3918        assert!(
3919            store
3920                .load_pending_compaction_projections(&rid)
3921                .await
3922                .unwrap()
3923                .is_empty()
3924        );
3925    }
3926
3927    #[tokio::test]
3928    async fn atomic_apply_roundtrip() {
3929        let store = InMemoryRuntimeStore::new();
3930        let run_id = RunId::new();
3931        let input_id = InputId::new();
3932
3933        let bundle = StoredInputState::new_accepted(input_id.clone());
3934        let receipt = make_receipt(run_id.clone(), 0);
3935
3936        let session = session_with_user("hello");
3937        let rid = LogicalRuntimeId::for_session(session.id());
3938        let session_snapshot = serde_json::to_vec(&session).unwrap();
3939
3940        store
3941            .atomic_apply(
3942                &rid,
3943                Some(SerializedSessionSnapshot {
3944                    session_snapshot: session_snapshot.into(),
3945                }),
3946                receipt.clone(),
3947                vec![persistable(bundle)],
3948                None,
3949            )
3950            .await
3951            .unwrap();
3952
3953        // Load input states
3954        let states = store.load_input_states_strict(&rid).await.unwrap();
3955        assert_eq!(states.len(), 1);
3956        assert_eq!(states[0].state.input_id, input_id);
3957
3958        // Load receipt
3959        let loaded = store.load_boundary_receipt(&rid, &run_id, 0).await.unwrap();
3960        assert!(loaded.is_some());
3961    }
3962
3963    #[tokio::test]
3964    async fn machine_terminal_atomic_apply_rolls_back_all_maps_on_receipt_conflict() {
3965        let store = InMemoryRuntimeStore::new();
3966        let session = session_with_user("must roll back");
3967        let rid = LogicalRuntimeId::for_session(session.id());
3968        let receipt = make_receipt(RunId::new(), 0);
3969        let seeded_input = StoredInputState::new_accepted(InputId::new());
3970        store
3971            .atomic_apply(
3972                &rid,
3973                None,
3974                receipt.clone(),
3975                vec![persistable(seeded_input.clone())],
3976                None,
3977            )
3978            .await
3979            .unwrap();
3980
3981        let replacement_input = StoredInputState::new_accepted(InputId::new());
3982        let error = store
3983            .atomic_apply_with_machine_lifecycle(
3984                &rid,
3985                SerializedSessionSnapshot {
3986                    session_snapshot: serde_json::to_vec(&session).unwrap().into(),
3987                },
3988                receipt,
3989                MachineLifecycleCommit::new_with_binding(
3990                    crate::RuntimeState::Idle,
3991                    crate::store::MachineLifecycleBindingFacts::default(),
3992                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
3993                ),
3994                vec![persistable(replacement_input)],
3995                session.id().clone(),
3996            )
3997            .await
3998            .expect_err("duplicate receipt must reject the entire terminal transaction");
3999        assert!(matches!(error, RuntimeStoreError::WriteFailed(_)));
4000        assert!(store.load_session_snapshot(&rid).await.unwrap().is_none());
4001        assert_eq!(
4002            crate::store::load_runtime_state(&store, &rid)
4003                .await
4004                .unwrap(),
4005            None
4006        );
4007        let inputs = store.load_input_states_strict(&rid).await.unwrap();
4008        assert_eq!(inputs.len(), 1);
4009        assert_eq!(inputs[0].state.input_id, seeded_input.state.input_id);
4010    }
4011
4012    #[tokio::test]
4013    async fn machine_terminal_atomic_apply_tracks_and_tombstones_compaction_intents() {
4014        let store = InMemoryRuntimeStore::new();
4015        let (session, intent) = session_with_compaction_intent();
4016        let rid = LogicalRuntimeId::for_session(session.id());
4017        let encoded = serde_json::to_vec(&session).unwrap();
4018
4019        store
4020            .atomic_apply_with_machine_lifecycle(
4021                &rid,
4022                SerializedSessionSnapshot {
4023                    session_snapshot: encoded.clone().into(),
4024                },
4025                make_receipt(RunId::new(), 0),
4026                MachineLifecycleCommit::new_with_binding(
4027                    crate::RuntimeState::Idle,
4028                    crate::store::MachineLifecycleBindingFacts::default(),
4029                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
4030                ),
4031                Vec::new(),
4032                session.id().clone(),
4033            )
4034            .await
4035            .unwrap();
4036        assert_eq!(
4037            store
4038                .load_pending_compaction_projections(&rid)
4039                .await
4040                .unwrap(),
4041            vec![intent.clone()]
4042        );
4043
4044        store
4045            .mark_compaction_projection_finalized(&rid, &intent.projection)
4046            .await
4047            .unwrap();
4048        let error = store
4049            .atomic_apply_with_machine_lifecycle(
4050                &rid,
4051                SerializedSessionSnapshot {
4052                    session_snapshot: encoded.into(),
4053                },
4054                make_receipt(RunId::new(), 1),
4055                MachineLifecycleCommit::new_with_binding(
4056                    crate::RuntimeState::Idle,
4057                    crate::store::MachineLifecycleBindingFacts::default(),
4058                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
4059                ),
4060                Vec::new(),
4061                session.id().clone(),
4062            )
4063            .await
4064            .expect_err("a finalized compaction tombstone must reject stale terminal replay");
4065        assert!(
4066            error
4067                .to_string()
4068                .contains("replays finalized compaction intent")
4069        );
4070    }
4071
4072    #[tokio::test]
4073    async fn machine_terminal_atomic_apply_replaces_orphan_body_and_commits_all_effects() {
4074        let store = InMemoryRuntimeStore::new();
4075        let session = session_with_user("incoming terminal transcript");
4076        let rid = LogicalRuntimeId::for_session(session.id());
4077        let corrupt = b"{not-a-session".to_vec();
4078        store
4079            .inner
4080            .lock()
4081            .await
4082            .sessions
4083            .insert(rid.0.clone(), corrupt.clone().into());
4084        assert!(matches!(
4085            store.load_committed_whole_blob_snapshot(&rid).await,
4086            Err(RuntimeStoreError::SessionPersistenceAuthorityConflict { .. })
4087        ));
4088        let receipt = make_receipt(RunId::new(), 0);
4089        let input_id = InputId::new();
4090        let session_snapshot = serde_json::to_vec(&session).unwrap();
4091        store
4092            .atomic_apply_with_machine_lifecycle(
4093                &rid,
4094                SerializedSessionSnapshot {
4095                    session_snapshot: session_snapshot.clone().into(),
4096                },
4097                receipt.clone(),
4098                MachineLifecycleCommit::new_with_binding(
4099                    crate::RuntimeState::Idle,
4100                    crate::store::MachineLifecycleBindingFacts::default(),
4101                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
4102                ),
4103                vec![persistable(StoredInputState::new_accepted(
4104                    input_id.clone(),
4105                ))],
4106                session.id().clone(),
4107            )
4108            .await
4109            .unwrap();
4110        assert_eq!(
4111            store.load_session_snapshot(&rid).await.unwrap(),
4112            Some(Arc::new(session_snapshot))
4113        );
4114        assert_eq!(
4115            crate::store::load_runtime_state(&store, &rid)
4116                .await
4117                .unwrap(),
4118            Some(crate::RuntimeState::Idle)
4119        );
4120        assert_eq!(
4121            store.load_input_states_strict(&rid).await.unwrap()[0]
4122                .state
4123                .input_id,
4124            input_id
4125        );
4126        assert_eq!(
4127            store
4128                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
4129                .await
4130                .unwrap(),
4131            Some(receipt)
4132        );
4133    }
4134
4135    #[tokio::test]
4136    async fn machine_terminal_atomic_apply_commits_target_state_and_publication_atomically() {
4137        let store = InMemoryRuntimeStore::new();
4138        let incoming = session_with_user("failed turn input");
4139        let rid = LogicalRuntimeId::for_session(incoming.id());
4140        let mut durable_head = incoming.clone();
4141        durable_head.push(meerkat_core::types::Message::User(
4142            meerkat_core::types::UserMessage::text("already advanced"),
4143        ));
4144        let durable_snapshot = serde_json::to_vec(&durable_head).unwrap();
4145        store
4146            .commit_session_snapshot(
4147                &rid,
4148                SerializedSessionSnapshot {
4149                    session_snapshot: durable_snapshot.clone().into(),
4150                },
4151            )
4152            .await
4153            .unwrap();
4154
4155        let receipt = make_receipt(RunId::new(), 0);
4156        let input_id = InputId::new();
4157        let incoming_snapshot = serde_json::to_vec(&incoming).unwrap();
4158        store
4159            .atomic_apply_with_machine_lifecycle(
4160                &rid,
4161                SerializedSessionSnapshot {
4162                    session_snapshot: incoming_snapshot.clone().into(),
4163                },
4164                receipt.clone(),
4165                MachineLifecycleCommit::new_with_binding(
4166                    crate::RuntimeState::Idle,
4167                    MachineLifecycleBindingFacts::default(),
4168                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
4169                ),
4170                vec![persistable(StoredInputState::new_accepted(
4171                    input_id.clone(),
4172                ))],
4173                incoming.id().clone(),
4174            )
4175            .await
4176            .unwrap();
4177        assert_eq!(
4178            store.load_session_snapshot(&rid).await.unwrap(),
4179            Some(Arc::new(incoming_snapshot))
4180        );
4181        assert_eq!(
4182            crate::store::load_runtime_state(&store, &rid)
4183                .await
4184                .unwrap(),
4185            Some(crate::RuntimeState::Idle)
4186        );
4187        assert_eq!(
4188            store.load_input_states_strict(&rid).await.unwrap()[0]
4189                .state
4190                .input_id,
4191            input_id
4192        );
4193        assert_eq!(
4194            store
4195                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
4196                .await
4197                .unwrap(),
4198            Some(receipt)
4199        );
4200    }
4201
4202    #[tokio::test]
4203    async fn atomic_apply_replaces_orphan_body_and_commits_all_effects() {
4204        let store = InMemoryRuntimeStore::new();
4205        let session = session_with_user("incoming transcript");
4206        let rid = LogicalRuntimeId::for_session(session.id());
4207        let corrupt = b"{not-a-session".to_vec();
4208        store
4209            .inner
4210            .lock()
4211            .await
4212            .sessions
4213            .insert(rid.0.clone(), corrupt.clone().into());
4214        assert!(matches!(
4215            store.load_committed_whole_blob_snapshot(&rid).await,
4216            Err(RuntimeStoreError::SessionPersistenceAuthorityConflict { .. })
4217        ));
4218        let receipt = make_receipt(RunId::new(), 0);
4219        let input_id = InputId::new();
4220        let session_snapshot = serde_json::to_vec(&session).unwrap();
4221        store
4222            .atomic_apply(
4223                &rid,
4224                Some(SerializedSessionSnapshot {
4225                    session_snapshot: session_snapshot.clone().into(),
4226                }),
4227                receipt.clone(),
4228                vec![persistable(StoredInputState::new_accepted(
4229                    input_id.clone(),
4230                ))],
4231                Some(session.id().clone()),
4232            )
4233            .await
4234            .unwrap();
4235        assert_eq!(
4236            store.load_session_snapshot(&rid).await.unwrap(),
4237            Some(Arc::new(session_snapshot))
4238        );
4239        assert_eq!(
4240            store.load_input_states_strict(&rid).await.unwrap()[0]
4241                .state
4242                .input_id,
4243            input_id
4244        );
4245        assert_eq!(
4246            store
4247                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
4248                .await
4249                .unwrap(),
4250            Some(receipt)
4251        );
4252    }
4253
4254    #[tokio::test]
4255    async fn atomic_apply_rejects_non_session_snapshot_without_owner_context() {
4256        let store = InMemoryRuntimeStore::new();
4257        let rid = LogicalRuntimeId::new("test-runtime");
4258        let run_id = RunId::new();
4259        let input_id = InputId::new();
4260
4261        let bundle = StoredInputState::new_accepted(input_id);
4262        let receipt = make_receipt(run_id, 0);
4263
4264        // Owner-context absence is not a license to store arbitrary bytes as a
4265        // session snapshot: a non-deserializable snapshot must fail closed.
4266        let err = store
4267            .atomic_apply(
4268                &rid,
4269                Some(SerializedSessionSnapshot {
4270                    session_snapshot: b"session-data".to_vec().into(),
4271                }),
4272                receipt,
4273                vec![persistable(bundle)],
4274                None,
4275            )
4276            .await
4277            .expect_err("non-Session snapshot must be rejected");
4278
4279        match err {
4280            RuntimeStoreError::WriteFailed(message) => {
4281                assert!(
4282                    message.contains("not a valid Session payload"),
4283                    "unexpected WriteFailed message: {message}"
4284                );
4285            }
4286            other => panic!("expected WriteFailed, got {other:?}"),
4287        }
4288    }
4289
4290    #[tokio::test]
4291    async fn persist_and_load_single_state() {
4292        let store = InMemoryRuntimeStore::new();
4293        let rid = LogicalRuntimeId::new("test");
4294        let input_id = InputId::new();
4295        let bundle = StoredInputState::new_accepted(input_id.clone());
4296
4297        store
4298            .persist_input_state(&rid, &persistable(bundle))
4299            .await
4300            .unwrap();
4301
4302        let loaded = store.load_input_state(&rid, &input_id).await.unwrap();
4303        assert!(loaded.is_some());
4304        assert_eq!(loaded.unwrap().state.input_id, input_id);
4305    }
4306
4307    fn replacement_records(
4308        expected: &[StoredInputState],
4309        recovery_count: u32,
4310    ) -> Vec<InputStatePersistenceRecord> {
4311        expected
4312            .iter()
4313            .cloned()
4314            .map(|mut row| {
4315                row.state.recovery_count = recovery_count;
4316                persistable(row)
4317            })
4318            .collect()
4319    }
4320
4321    #[tokio::test]
4322    async fn input_idempotency_mutations_use_complete_final_image() {
4323        let store = InMemoryRuntimeStore::new();
4324        crate::store::assert_input_idempotency_final_image_contract(&store).await;
4325    }
4326
4327    #[tokio::test]
4328    async fn input_idempotency_corruption_uses_typed_uncertainty() {
4329        let store = InMemoryRuntimeStore::new();
4330        let dangling_runtime = LogicalRuntimeId::new("memory-dangling-idempotency");
4331        let dangling_input_id = InputId::new();
4332        {
4333            let mut inner = store.inner.lock().await;
4334            inner
4335                .input_idempotency_index
4336                .entry(dangling_runtime.0.clone())
4337                .or_default()
4338                .insert("dangling-key".to_string(), dangling_input_id.clone());
4339        }
4340        assert!(matches!(
4341            store
4342                .load_input_state_by_idempotency_key(
4343                    &dangling_runtime,
4344                    &IdempotencyKey::new("dangling-key"),
4345                )
4346                .await,
4347            Err(RuntimeStoreError::InputIdempotencyIndexUncertain {
4348                evidence_input_id,
4349                reason,
4350                ..
4351            }) if evidence_input_id == dangling_input_id.to_string()
4352                && reason.contains("missing source input row")
4353        ));
4354
4355        let invalid_seed_runtime = LogicalRuntimeId::new("memory-invalid-idempotency-seed");
4356        let invalid_seed_input_id = InputId::new();
4357        let mut invalid_seed = StoredInputState::new_accepted(invalid_seed_input_id.clone());
4358        invalid_seed.state.idempotency_key = Some(IdempotencyKey::new("invalid-seed-key"));
4359        invalid_seed.seed.terminal_outcome =
4360            Some(crate::input_state::InputTerminalOutcome::Consumed);
4361        {
4362            let mut inner = store.inner.lock().await;
4363            inner
4364                .input_states
4365                .entry(invalid_seed_runtime.0.clone())
4366                .or_default()
4367                .insert(invalid_seed_input_id.clone(), invalid_seed);
4368            inner
4369                .input_idempotency_index
4370                .entry(invalid_seed_runtime.0.clone())
4371                .or_default()
4372                .insert(
4373                    "invalid-seed-key".to_string(),
4374                    invalid_seed_input_id.clone(),
4375                );
4376        }
4377        assert!(matches!(
4378            store
4379                .load_input_state_by_idempotency_key(
4380                    &invalid_seed_runtime,
4381                    &IdempotencyKey::new("invalid-seed-key"),
4382                )
4383                .await,
4384            Err(RuntimeStoreError::InputIdempotencyIndexUncertain {
4385                evidence_input_id,
4386                reason,
4387                ..
4388            }) if evidence_input_id == invalid_seed_input_id.to_string()
4389                && reason.contains("non-authoritative machine seed")
4390        ));
4391    }
4392
4393    #[tokio::test]
4394    async fn input_state_batch_cas_memory_swaps_once_and_stale_is_noop() {
4395        let store = InMemoryRuntimeStore::new();
4396        let rid = LogicalRuntimeId::new("input-cas-memory");
4397        let expected: Vec<_> = (0..3)
4398            .map(|_| StoredInputState::new_accepted(InputId::new()))
4399            .collect();
4400        let initial: Vec<_> = expected.iter().cloned().map(persistable).collect();
4401        store
4402            .persist_input_states_atomically(&rid, &initial)
4403            .await
4404            .unwrap();
4405
4406        let winner = replacement_records(&expected, 1);
4407        let stale_candidate = replacement_records(&expected, 2);
4408        assert_eq!(
4409            store
4410                .compare_and_swap_input_states_atomically(&rid, &expected, &winner)
4411                .await
4412                .unwrap(),
4413            InputStateBatchCasOutcome::Swapped
4414        );
4415        assert_eq!(
4416            store
4417                .compare_and_swap_input_states_atomically(&rid, &expected, &winner)
4418                .await
4419                .unwrap(),
4420            InputStateBatchCasOutcome::Swapped,
4421            "retry after a lost CAS acknowledgement must observe the exact replacement as success"
4422        );
4423        assert_eq!(
4424            store
4425                .compare_and_swap_input_states_atomically(&rid, &expected, &stale_candidate)
4426                .await
4427                .unwrap(),
4428            InputStateBatchCasOutcome::Stale
4429        );
4430        let rows = store.load_input_states_strict(&rid).await.unwrap();
4431        assert_eq!(rows.len(), 3);
4432        assert!(rows.iter().all(|row| row.state.recovery_count == 1));
4433    }
4434
4435    #[tokio::test]
4436    async fn input_state_batch_cas_memory_rejects_missing_extra_and_key_mismatch() {
4437        let store = InMemoryRuntimeStore::new();
4438        let rid = LogicalRuntimeId::new("input-cas-shape");
4439        let expected: Vec<_> = (0..2)
4440            .map(|_| StoredInputState::new_accepted(InputId::new()))
4441            .collect();
4442        store
4443            .persist_input_state(&rid, &persistable(expected[0].clone()))
4444            .await
4445            .unwrap();
4446        let replacements = replacement_records(&expected, 1);
4447
4448        assert_eq!(
4449            store
4450                .compare_and_swap_input_states_atomically(&rid, &expected, &replacements)
4451                .await
4452                .unwrap(),
4453            InputStateBatchCasOutcome::Stale,
4454            "one missing durable row must stale the entire batch"
4455        );
4456        assert_eq!(
4457            store
4458                .load_input_state(&rid, &expected[0].state.input_id)
4459                .await
4460                .unwrap()
4461                .unwrap()
4462                .state
4463                .recovery_count,
4464            0,
4465            "stale comparison must not update the matching prefix row"
4466        );
4467
4468        let extra = vec![
4469            replacements[0].clone(),
4470            replacements[1].clone(),
4471            persistable(StoredInputState::new_accepted(InputId::new())),
4472        ];
4473        assert!(matches!(
4474            store
4475                .compare_and_swap_input_states_atomically(&rid, &expected, &extra)
4476                .await,
4477            Err(RuntimeStoreError::InvalidInputStateBatchCas { .. })
4478        ));
4479
4480        let wrong_key = vec![
4481            replacements[0].clone(),
4482            persistable(StoredInputState::new_accepted(InputId::new())),
4483        ];
4484        assert!(matches!(
4485            store
4486                .compare_and_swap_input_states_atomically(&rid, &expected, &wrong_key)
4487                .await,
4488            Err(RuntimeStoreError::InvalidInputStateBatchCas { .. })
4489        ));
4490    }
4491
4492    #[tokio::test]
4493    async fn load_nonexistent_returns_none() {
4494        let store = InMemoryRuntimeStore::new();
4495        let rid = LogicalRuntimeId::new("test");
4496
4497        let states = store.load_input_states_strict(&rid).await.unwrap();
4498        assert!(states.is_empty());
4499
4500        let state = store.load_input_state(&rid, &InputId::new()).await.unwrap();
4501        assert!(state.is_none());
4502
4503        let receipt = store
4504            .load_boundary_receipt(&rid, &RunId::new(), 0)
4505            .await
4506            .unwrap();
4507        assert!(receipt.is_none());
4508    }
4509
4510    #[tokio::test]
4511    async fn atomic_apply_updates_existing() {
4512        let store = InMemoryRuntimeStore::new();
4513        let rid = LogicalRuntimeId::new("test");
4514        let input_id = InputId::new();
4515
4516        // First write
4517        let bundle1 = StoredInputState::new_accepted(input_id.clone());
4518        store
4519            .atomic_apply(
4520                &rid,
4521                None,
4522                make_receipt(RunId::new(), 0),
4523                vec![persistable(bundle1)],
4524                None,
4525            )
4526            .await
4527            .unwrap();
4528
4529        // Second write with updated seed phase
4530        let mut bundle2 = StoredInputState::new_accepted(input_id.clone());
4531        bundle2.seed.phase = crate::input_state::InputLifecycleState::Queued;
4532        store
4533            .atomic_apply(
4534                &rid,
4535                None,
4536                make_receipt(RunId::new(), 1),
4537                vec![persistable(bundle2)],
4538                None,
4539            )
4540            .await
4541            .unwrap();
4542
4543        let states = store.load_input_states_strict(&rid).await.unwrap();
4544        assert_eq!(states.len(), 1);
4545        assert_eq!(
4546            states[0].seed.phase,
4547            crate::input_state::InputLifecycleState::Queued
4548        );
4549    }
4550
4551    #[tokio::test]
4552    async fn atomic_apply_validates_session_store_key_without_aliasing_snapshot() {
4553        let store = InMemoryRuntimeStore::new();
4554        let session = meerkat_core::Session::new();
4555        let rid = LogicalRuntimeId::for_session(session.id());
4556        let session_id = session.id().clone();
4557        let snapshot = serde_json::to_vec(&session).unwrap();
4558
4559        store
4560            .atomic_apply(
4561                &rid,
4562                Some(SerializedSessionSnapshot {
4563                    session_snapshot: snapshot.clone().into(),
4564                }),
4565                make_receipt(RunId::new(), 0),
4566                vec![],
4567                Some(session_id.clone()),
4568            )
4569            .await
4570            .unwrap();
4571
4572        assert_eq!(
4573            store.load_session_snapshot(&rid).await.unwrap(),
4574            Some(Arc::new(snapshot))
4575        );
4576        assert!(
4577            store
4578                .load_session_snapshot(&LogicalRuntimeId::legacy_session_uuid_alias(&session_id))
4579                .await
4580                .unwrap()
4581                .is_none(),
4582            "session_store_key must validate the snapshot identity, not create a raw UUID runtime alias"
4583        );
4584    }
4585
4586    #[tokio::test]
4587    async fn atomic_apply_rejects_mismatched_session_store_key() {
4588        let store = InMemoryRuntimeStore::new();
4589        let session = meerkat_core::Session::new();
4590        let rid = LogicalRuntimeId::for_session(session.id());
4591        let wrong_session_id = meerkat_core::Session::new().id().clone();
4592        let snapshot = serde_json::to_vec(&session).unwrap();
4593
4594        let err = store
4595            .atomic_apply(
4596                &rid,
4597                Some(SerializedSessionSnapshot {
4598                    session_snapshot: snapshot.into(),
4599                }),
4600                make_receipt(RunId::new(), 0),
4601                vec![],
4602                Some(wrong_session_id),
4603            )
4604            .await
4605            .expect_err("mismatched session_store_key should fail");
4606
4607        assert!(matches!(err, RuntimeStoreError::SessionKeyMismatch { .. }));
4608        assert!(store.load_session_snapshot(&rid).await.unwrap().is_none());
4609    }
4610
4611    #[tokio::test]
4612    async fn typed_whole_blob_snapshot_cas_uses_exact_store_authority_and_can_compensate() {
4613        let store = InMemoryRuntimeStore::new();
4614        let mut predecessor = meerkat_core::Session::new();
4615        predecessor.append_system_message("before".to_string());
4616        let runtime_id = LogicalRuntimeId::for_session(predecessor.id());
4617        store
4618            .commit_session_snapshot(
4619                &runtime_id,
4620                SerializedSessionSnapshot {
4621                    session_snapshot: serde_json::to_vec(&predecessor).unwrap().into(),
4622                },
4623            )
4624            .await
4625            .unwrap();
4626
4627        let committed = store
4628            .load_committed_whole_blob_snapshot(&runtime_id)
4629            .await
4630            .unwrap()
4631            .unwrap();
4632        let base_authority = committed.authority().clone();
4633        let predecessor = committed.session_arc();
4634        let predecessor_messages = predecessor.messages().to_vec();
4635        let mut successor = predecessor.as_ref().clone();
4636        successor.append_system_message("after".to_string());
4637        let prepared = PreparedWholeBlobSnapshotCas::prepare(
4638            base_authority.clone(),
4639            meerkat_core::lifecycle::core_executor::BoundSessionCommit::sealed(Arc::new(successor))
4640                .unwrap(),
4641        )
4642        .unwrap();
4643        let target_authority = match store
4644            .commit_prepared_whole_blob_snapshot_cas(&runtime_id, prepared.clone())
4645            .await
4646            .unwrap()
4647        {
4648            WholeBlobSnapshotCasOutcome::Committed(authority) => authority,
4649            WholeBlobSnapshotCasOutcome::Conflict => panic!("exact predecessor must commit"),
4650        };
4651        assert!(prepared.accepts_committed_authority(&target_authority));
4652
4653        let stale = PreparedWholeBlobSnapshotCas::prepare(
4654            base_authority,
4655            meerkat_core::lifecycle::core_executor::BoundSessionCommit::sealed(Arc::clone(
4656                &predecessor,
4657            ))
4658            .unwrap(),
4659        )
4660        .unwrap();
4661        assert_eq!(
4662            store
4663                .commit_prepared_whole_blob_snapshot_cas(&runtime_id, stale)
4664                .await
4665                .unwrap(),
4666            WholeBlobSnapshotCasOutcome::Conflict
4667        );
4668
4669        let compensation = PreparedWholeBlobSnapshotCas::prepare(
4670            target_authority,
4671            meerkat_core::lifecycle::core_executor::BoundSessionCommit::sealed(predecessor)
4672                .unwrap(),
4673        )
4674        .unwrap();
4675        let restored = store
4676            .commit_prepared_whole_blob_snapshot_cas(&runtime_id, compensation.clone())
4677            .await
4678            .unwrap();
4679        let WholeBlobSnapshotCasOutcome::Committed(restored_authority) = restored else {
4680            panic!("exact target authority must permit compensation");
4681        };
4682        assert!(compensation.accepts_committed_authority(&restored_authority));
4683        let restored = store
4684            .load_committed_whole_blob_snapshot(&runtime_id)
4685            .await
4686            .unwrap()
4687            .unwrap();
4688        assert_eq!(
4689            restored.session().messages(),
4690            predecessor_messages.as_slice()
4691        );
4692    }
4693
4694    #[tokio::test]
4695    async fn atomic_apply_persists_machine_owned_receipt() {
4696        let store = InMemoryRuntimeStore::new();
4697        let run_id = RunId::new();
4698        let input_id = InputId::new();
4699        let session = meerkat_core::Session::new();
4700        let rid = LogicalRuntimeId::for_session(session.id());
4701        let snapshot = serde_json::to_vec(&session).unwrap();
4702        let receipt = RunBoundaryReceipt {
4703            run_id: run_id.clone(),
4704            boundary: RunApplyBoundary::Immediate,
4705            contributing_input_ids: vec![input_id.clone()],
4706            conversation_digest: Some("machine-owned-digest".to_string()),
4707            message_count: 42,
4708            sequence: 7,
4709        };
4710
4711        store
4712            .atomic_apply(
4713                &rid,
4714                Some(SerializedSessionSnapshot {
4715                    session_snapshot: snapshot.into(),
4716                }),
4717                receipt.clone(),
4718                vec![persistable(StoredInputState::new_accepted(input_id))],
4719                None,
4720            )
4721            .await
4722            .unwrap();
4723
4724        assert_eq!(receipt.run_id, run_id);
4725        assert!(receipt.conversation_digest.is_some());
4726        let loaded = store
4727            .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
4728            .await
4729            .unwrap();
4730        assert!(loaded.is_some(), "receipt should be persisted");
4731        let Some(loaded) = loaded else {
4732            unreachable!("asserted above");
4733        };
4734        assert_eq!(loaded, receipt);
4735    }
4736
4737    #[tokio::test]
4738    async fn multiple_runtimes_isolated() {
4739        let store = InMemoryRuntimeStore::new();
4740        let rid1 = LogicalRuntimeId::new("runtime-1");
4741        let rid2 = LogicalRuntimeId::new("runtime-2");
4742
4743        store
4744            .persist_input_state(
4745                &rid1,
4746                &persistable(StoredInputState::new_accepted(InputId::new())),
4747            )
4748            .await
4749            .unwrap();
4750        store
4751            .persist_input_state(
4752                &rid2,
4753                &persistable(StoredInputState::new_accepted(InputId::new())),
4754            )
4755            .await
4756            .unwrap();
4757        store
4758            .persist_input_state(
4759                &rid2,
4760                &persistable(StoredInputState::new_accepted(InputId::new())),
4761            )
4762            .await
4763            .unwrap();
4764
4765        let s1 = store.load_input_states_strict(&rid1).await.unwrap();
4766        let s2 = store.load_input_states_strict(&rid2).await.unwrap();
4767        assert_eq!(s1.len(), 1);
4768        assert_eq!(s2.len(), 2);
4769    }
4770
4771    #[tokio::test]
4772    async fn load_session_snapshot_roundtrip() {
4773        let store = InMemoryRuntimeStore::new();
4774        let session = meerkat_core::Session::new();
4775        let rid = LogicalRuntimeId::for_session(session.id());
4776        let snapshot = serde_json::to_vec(&session).unwrap();
4777
4778        store
4779            .atomic_apply(
4780                &rid,
4781                Some(SerializedSessionSnapshot {
4782                    session_snapshot: snapshot.clone().into(),
4783                }),
4784                make_receipt(RunId::new(), 0),
4785                vec![],
4786                None,
4787            )
4788            .await
4789            .unwrap();
4790
4791        let loaded = store.load_session_snapshot(&rid).await.unwrap();
4792        assert_eq!(loaded, Some(Arc::new(snapshot)));
4793    }
4794
4795    #[tokio::test]
4796    async fn typed_whole_blob_snapshot_cas_rejects_stale_runtime_parent() {
4797        let store = InMemoryRuntimeStore::new();
4798        let accepted = session_with_user("accepted runtime turn");
4799        let rid = LogicalRuntimeId::for_session(accepted.id());
4800        let mut stale = meerkat_core::Session::with_id(accepted.id().clone());
4801        stale.push(meerkat_core::types::Message::User(
4802            meerkat_core::types::UserMessage::text("stale runtime turn".to_string()),
4803        ));
4804        let accepted_snapshot = serde_json::to_vec(&accepted).unwrap();
4805
4806        store
4807            .commit_session_snapshot(
4808                &rid,
4809                SerializedSessionSnapshot {
4810                    session_snapshot: accepted_snapshot.clone().into(),
4811                },
4812            )
4813            .await
4814            .unwrap();
4815
4816        let base = store
4817            .load_committed_whole_blob_snapshot(&rid)
4818            .await
4819            .unwrap()
4820            .unwrap()
4821            .authority()
4822            .clone();
4823        let mut advanced = accepted.clone();
4824        advanced.push(meerkat_core::types::Message::User(
4825            meerkat_core::types::UserMessage::text("accepted continuation"),
4826        ));
4827        let advanced_snapshot = serde_json::to_vec(&advanced).unwrap();
4828        let advance = PreparedWholeBlobSnapshotCas::prepare(
4829            base.clone(),
4830            meerkat_core::lifecycle::core_executor::BoundSessionCommit::sealed(Arc::new(advanced))
4831                .unwrap(),
4832        )
4833        .unwrap();
4834        assert!(matches!(
4835            store
4836                .commit_prepared_whole_blob_snapshot_cas(&rid, advance)
4837                .await
4838                .unwrap(),
4839            WholeBlobSnapshotCasOutcome::Committed(_)
4840        ));
4841
4842        let stale = PreparedWholeBlobSnapshotCas::prepare(
4843            base,
4844            meerkat_core::lifecycle::core_executor::BoundSessionCommit::sealed(Arc::new(stale))
4845                .unwrap(),
4846        )
4847        .unwrap();
4848        assert_eq!(
4849            store
4850                .commit_prepared_whole_blob_snapshot_cas(&rid, stale)
4851                .await
4852                .unwrap(),
4853            WholeBlobSnapshotCasOutcome::Conflict
4854        );
4855        assert_eq!(
4856            store.load_session_snapshot(&rid).await.unwrap(),
4857            Some(Arc::new(advanced_snapshot))
4858        );
4859    }
4860
4861    #[tokio::test]
4862    async fn atomic_apply_commits_target_state_and_receipt_as_one_boundary() {
4863        let store = InMemoryRuntimeStore::new();
4864        let incoming = session_with_user("turn input");
4865        let rid = LogicalRuntimeId::for_session(incoming.id());
4866        let mut current = incoming.clone();
4867        current.push(meerkat_core::types::Message::BlockAssistant(
4868            meerkat_core::types::BlockAssistantMessage {
4869                blocks: vec![meerkat_core::types::AssistantBlock::Text {
4870                    text: "peer response already applied".to_string(),
4871                    meta: None,
4872                }],
4873                stop_reason: meerkat_core::types::StopReason::EndTurn,
4874                identity: meerkat_core::types::TranscriptMessageIdentity::default(),
4875                created_at: meerkat_core::types::message_timestamp_now(),
4876            },
4877        ));
4878        let current_snapshot = serde_json::to_vec(&current).unwrap();
4879        let incoming_snapshot = serde_json::to_vec(&incoming).unwrap();
4880        let receipt = make_receipt(RunId::new(), 11);
4881
4882        store
4883            .commit_session_snapshot(
4884                &rid,
4885                SerializedSessionSnapshot {
4886                    session_snapshot: current_snapshot.clone().into(),
4887                },
4888            )
4889            .await
4890            .unwrap();
4891
4892        store
4893            .atomic_apply(
4894                &rid,
4895                Some(SerializedSessionSnapshot {
4896                    session_snapshot: incoming_snapshot.clone().into(),
4897                }),
4898                receipt.clone(),
4899                vec![],
4900                Some(incoming.id().clone()),
4901            )
4902            .await
4903            .unwrap();
4904
4905        assert_eq!(
4906            store.load_session_snapshot(&rid).await.unwrap(),
4907            Some(Arc::new(incoming_snapshot))
4908        );
4909        assert_eq!(
4910            store
4911                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
4912                .await
4913                .unwrap(),
4914            Some(receipt)
4915        );
4916    }
4917
4918    #[tokio::test]
4919    async fn atomic_apply_commits_target_state_receipt_and_inputs_as_one_boundary() {
4920        let store = InMemoryRuntimeStore::new();
4921        let incoming = session_with_user("turn input");
4922        let rid = LogicalRuntimeId::for_session(incoming.id());
4923        let mut current = incoming.clone();
4924        current.push(meerkat_core::types::Message::BlockAssistant(
4925            meerkat_core::types::BlockAssistantMessage {
4926                blocks: vec![meerkat_core::types::AssistantBlock::Text {
4927                    text: "peer response already applied".to_string(),
4928                    meta: None,
4929                }],
4930                stop_reason: meerkat_core::types::StopReason::EndTurn,
4931                identity: meerkat_core::types::TranscriptMessageIdentity::default(),
4932                created_at: meerkat_core::types::message_timestamp_now(),
4933            },
4934        ));
4935        let current_snapshot = serde_json::to_vec(&current).unwrap();
4936        let incoming_snapshot = serde_json::to_vec(&incoming).unwrap();
4937        let receipt = make_receipt(RunId::new(), 21);
4938        let input_id = InputId::new();
4939        let bundle = StoredInputState::new_accepted(input_id.clone());
4940
4941        store
4942            .commit_session_snapshot(
4943                &rid,
4944                SerializedSessionSnapshot {
4945                    session_snapshot: current_snapshot.clone().into(),
4946                },
4947            )
4948            .await
4949            .unwrap();
4950
4951        store
4952            .atomic_apply(
4953                &rid,
4954                Some(SerializedSessionSnapshot {
4955                    session_snapshot: incoming_snapshot.clone().into(),
4956                }),
4957                receipt.clone(),
4958                vec![persistable(bundle)],
4959                Some(incoming.id().clone()),
4960            )
4961            .await
4962            .unwrap();
4963
4964        assert_eq!(
4965            store.load_session_snapshot(&rid).await.unwrap(),
4966            Some(Arc::new(incoming_snapshot))
4967        );
4968        assert_eq!(
4969            store
4970                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
4971                .await
4972                .unwrap(),
4973            Some(receipt)
4974        );
4975        assert_eq!(
4976            store.load_input_states_strict(&rid).await.unwrap()[0]
4977                .state
4978                .input_id,
4979            input_id
4980        );
4981    }
4982
4983    #[tokio::test]
4984    async fn atomic_apply_allows_first_generated_snapshot_after_placeholder() {
4985        let store = InMemoryRuntimeStore::new();
4986        let mut placeholder = meerkat_core::Session::new();
4987        let rid = LogicalRuntimeId::for_session(placeholder.id());
4988        placeholder.append_system_message("base system".to_string());
4989        let mut incoming = meerkat_core::Session::with_id(placeholder.id().clone());
4990        incoming.append_system_message("base system".to_string());
4991        incoming.push(meerkat_core::types::Message::User(
4992            meerkat_core::types::UserMessage::text("verbose first turn".to_string()),
4993        ));
4994        let parent_revision = incoming.transcript_revision().unwrap();
4995        incoming
4996            .commit_transcript_rewrite(
4997                meerkat_core::TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
4998                vec![meerkat_core::types::Message::User(
4999                    meerkat_core::types::UserMessage::compaction_summary(
5000                        "[Context compacted] first turn",
5001                    ),
5002                )],
5003                meerkat_core::TranscriptRewriteReason::new("compaction"),
5004                Some("meerkat-core".to_string()),
5005                Some(parent_revision),
5006            )
5007            .unwrap();
5008        let incoming_snapshot = serde_json::to_vec(&incoming).unwrap();
5009        let receipt = make_receipt(RunId::new(), 12);
5010
5011        store
5012            .commit_session_snapshot(
5013                &rid,
5014                SerializedSessionSnapshot {
5015                    session_snapshot: serde_json::to_vec(&placeholder).unwrap().into(),
5016                },
5017            )
5018            .await
5019            .unwrap();
5020
5021        store
5022            .atomic_apply(
5023                &rid,
5024                Some(SerializedSessionSnapshot {
5025                    session_snapshot: incoming_snapshot.clone().into(),
5026                }),
5027                receipt.clone(),
5028                vec![],
5029                Some(incoming.id().clone()),
5030            )
5031            .await
5032            .unwrap();
5033
5034        assert_eq!(
5035            store.load_session_snapshot(&rid).await.unwrap(),
5036            Some(Arc::new(incoming_snapshot))
5037        );
5038        assert_eq!(
5039            store
5040                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
5041                .await
5042                .unwrap(),
5043            Some(receipt)
5044        );
5045    }
5046
5047    #[tokio::test]
5048    async fn atomic_apply_allows_generated_compaction_before_retained_tail() {
5049        let store = InMemoryRuntimeStore::new();
5050        let mut previous = meerkat_core::Session::new();
5051        let rid = LogicalRuntimeId::for_session(previous.id());
5052        previous.append_system_message("runtime system before context refresh".to_string());
5053        previous.push(meerkat_core::types::Message::User(
5054            meerkat_core::types::UserMessage::text("Turn 1 request".to_string()),
5055        ));
5056        previous.push(meerkat_core::types::Message::BlockAssistant(
5057            meerkat_core::types::BlockAssistantMessage {
5058                blocks: vec![meerkat_core::types::AssistantBlock::Text {
5059                    text: "Turn 1 answer".to_string(),
5060                    meta: None,
5061                }],
5062                stop_reason: meerkat_core::types::StopReason::EndTurn,
5063                identity: meerkat_core::types::TranscriptMessageIdentity::default(),
5064                created_at: meerkat_core::types::message_timestamp_now(),
5065            },
5066        ));
5067
5068        let mut incoming = meerkat_core::Session::with_id(previous.id().clone());
5069        incoming.append_system_message("runtime system after context refresh".to_string());
5070        incoming.push(meerkat_core::types::Message::User(
5071            meerkat_core::types::UserMessage::text(
5072                "Verbose context that will be compacted".to_string(),
5073            ),
5074        ));
5075        for message in previous.messages()[1..].iter().cloned() {
5076            incoming.push(message);
5077        }
5078        incoming.push(meerkat_core::types::Message::BlockAssistant(
5079            meerkat_core::types::BlockAssistantMessage {
5080                blocks: vec![meerkat_core::types::AssistantBlock::Text {
5081                    text: "Turn 2 generated answer".to_string(),
5082                    meta: None,
5083                }],
5084                stop_reason: meerkat_core::types::StopReason::EndTurn,
5085                identity: meerkat_core::types::TranscriptMessageIdentity::default(),
5086                created_at: meerkat_core::types::message_timestamp_now(),
5087            },
5088        ));
5089        let parent_revision = incoming.transcript_revision().unwrap();
5090        incoming
5091            .commit_transcript_rewrite(
5092                meerkat_core::TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
5093                vec![meerkat_core::types::Message::User(
5094                    meerkat_core::types::UserMessage::compaction_summary(
5095                        "[Context compacted] Earlier runtime context".to_string(),
5096                    ),
5097                )],
5098                meerkat_core::TranscriptRewriteReason::new("compaction"),
5099                Some("meerkat-core".to_string()),
5100                Some(parent_revision),
5101            )
5102            .unwrap();
5103        let incoming_snapshot = serde_json::to_vec(&incoming).unwrap();
5104        let receipt = make_receipt(RunId::new(), 13);
5105
5106        store
5107            .commit_session_snapshot(
5108                &rid,
5109                SerializedSessionSnapshot {
5110                    session_snapshot: serde_json::to_vec(&previous).unwrap().into(),
5111                },
5112            )
5113            .await
5114            .unwrap();
5115
5116        store
5117            .atomic_apply(
5118                &rid,
5119                Some(SerializedSessionSnapshot {
5120                    session_snapshot: incoming_snapshot.clone().into(),
5121                }),
5122                receipt.clone(),
5123                vec![],
5124                Some(incoming.id().clone()),
5125            )
5126            .await
5127            .unwrap();
5128
5129        assert_eq!(
5130            store.load_session_snapshot(&rid).await.unwrap(),
5131            Some(Arc::new(incoming_snapshot))
5132        );
5133        assert_eq!(
5134            store
5135                .load_boundary_receipt(&rid, &receipt.run_id, receipt.sequence)
5136                .await
5137                .unwrap(),
5138            Some(receipt)
5139        );
5140    }
5141
5142    #[tokio::test]
5143    async fn commit_machine_lifecycle_persists_binding_facts() {
5144        use crate::runtime_state::RuntimeState;
5145
5146        let store = InMemoryRuntimeStore::new();
5147        let rid = LogicalRuntimeId::new("runtime-binding");
5148        let binding = MachineLifecycleBindingFacts::new(
5149            Some("rt:session:abc".to_string()),
5150            Some(7),
5151            Some(3),
5152            Some("epoch-1".to_string()),
5153        );
5154
5155        store
5156            .commit_machine_lifecycle(
5157                &rid,
5158                MachineLifecycleCommit::new_with_binding(
5159                    RuntimeState::Retired,
5160                    binding.clone(),
5161                    crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
5162                ),
5163                &[],
5164            )
5165            .await
5166            .unwrap();
5167
5168        let lifecycle = crate::store::load_machine_lifecycle(&store, &rid)
5169            .await
5170            .unwrap()
5171            .expect("machine lifecycle snapshot");
5172        assert_eq!(lifecycle.runtime_state(), RuntimeState::Retired);
5173        assert_eq!(lifecycle.binding(), &binding);
5174        assert_eq!(
5175            crate::store::load_runtime_state(&store, &rid)
5176                .await
5177                .unwrap(),
5178            Some(RuntimeState::Retired)
5179        );
5180    }
5181
5182    #[tokio::test]
5183    async fn lifecycle_publications_advance_existing_session_catalog() {
5184        let store = InMemoryRuntimeStore::new();
5185        let session = session_with_user("catalog lifecycle");
5186        let runtime_id = LogicalRuntimeId::for_session(session.id());
5187        store
5188            .commit_session_snapshot(
5189                &runtime_id,
5190                SerializedSessionSnapshot {
5191                    session_snapshot: session.to_persisted_bytes().unwrap().into(),
5192                },
5193            )
5194            .await
5195            .unwrap();
5196        assert_eq!(
5197            store
5198                .load_runtime_session_catalog_entry(&runtime_id)
5199                .await
5200                .unwrap()
5201                .unwrap()
5202                .runtime_state(),
5203            None
5204        );
5205
5206        let MachineLifecycleCasOutcome::Applied { .. } = store
5207            .compare_and_swap_machine_lifecycle(
5208                &runtime_id,
5209                MachineLifecycleExpectedVersion::Missing,
5210                lifecycle_commit(&runtime_id, RuntimeState::Idle, 7, 3),
5211            )
5212            .await
5213            .unwrap()
5214        else {
5215            panic!("missing lifecycle must be installed");
5216        };
5217        assert_eq!(
5218            store
5219                .load_runtime_session_catalog_entry(&runtime_id)
5220                .await
5221                .unwrap()
5222                .unwrap()
5223                .runtime_state(),
5224            Some(RuntimeState::Idle)
5225        );
5226
5227        store
5228            .commit_machine_lifecycle(
5229                &runtime_id,
5230                lifecycle_commit(&runtime_id, RuntimeState::Retired, 8, 4),
5231                &[],
5232            )
5233            .await
5234            .unwrap();
5235        assert_eq!(
5236            store
5237                .load_runtime_session_catalog_entry(&runtime_id)
5238                .await
5239                .unwrap()
5240                .unwrap()
5241                .runtime_state(),
5242            Some(RuntimeState::Retired)
5243        );
5244
5245        store
5246            .inner
5247            .lock()
5248            .await
5249            .session_catalog
5250            .get_mut(&runtime_id.0)
5251            .expect("catalog entry")
5252            .set_runtime_state(Some(RuntimeState::Idle));
5253        let MachineLifecycleObservation::Decoded { version, .. } =
5254            store.observe_machine_lifecycle(&runtime_id).await.unwrap()
5255        else {
5256            panic!("retired lifecycle must decode");
5257        };
5258        assert!(matches!(
5259            store
5260                .compare_and_swap_machine_lifecycle_with_fence(
5261                    &runtime_id,
5262                    MachineLifecycleExpectedVersion::Version(version),
5263                    lifecycle_commit(&runtime_id, RuntimeState::Retired, 8, 4),
5264                    Arc::new(AppliedWriteFence),
5265                )
5266                .await
5267                .unwrap(),
5268            FencedMachineLifecycleCasOutcome::AlreadyExact { .. }
5269        ));
5270        assert_eq!(
5271            store
5272                .load_runtime_session_catalog_entry(&runtime_id)
5273                .await
5274                .unwrap()
5275                .unwrap()
5276                .runtime_state(),
5277            Some(RuntimeState::Retired),
5278            "applied already-exact fence must heal a stale catalog"
5279        );
5280
5281        let ops_snapshot = crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new()
5282            .capture_persistence_snapshot(
5283                meerkat_core::RuntimeEpochId::new(),
5284                &meerkat_core::EpochCursorState::new(),
5285            )
5286            .unwrap();
5287        store
5288            .persist_ops_lifecycle(&runtime_id, &ops_snapshot)
5289            .await
5290            .unwrap();
5291        store
5292            .commit_unregister_finalization(
5293                &runtime_id,
5294                crate::store::UnregisterFinalizationCommit::new(
5295                    lifecycle_commit(&runtime_id, RuntimeState::Destroyed, 9, 5),
5296                    vec![],
5297                    ops_snapshot.epoch_id,
5298                    crate::meerkat_machine::DeleteOpsFinalizationAuthority::for_store_test(),
5299                ),
5300            )
5301            .await
5302            .unwrap();
5303        assert_eq!(
5304            store
5305                .load_runtime_session_catalog_entry(&runtime_id)
5306                .await
5307                .unwrap()
5308                .unwrap()
5309                .runtime_state(),
5310            Some(RuntimeState::Destroyed)
5311        );
5312    }
5313
5314    #[tokio::test]
5315    async fn concurrent_ops_initializers_return_one_canonical_snapshot() {
5316        let store = InMemoryRuntimeStore::new();
5317        let runtime_id = LogicalRuntimeId::new("runtime-concurrent-ops-initialize");
5318        let registry = crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new();
5319        let first_candidate = registry
5320            .capture_persistence_snapshot(
5321                meerkat_core::RuntimeEpochId::new(),
5322                &meerkat_core::EpochCursorState::new(),
5323            )
5324            .unwrap();
5325        let second_candidate = registry
5326            .capture_persistence_snapshot(
5327                meerkat_core::RuntimeEpochId::new(),
5328                &meerkat_core::EpochCursorState::new(),
5329            )
5330            .unwrap();
5331        assert_ne!(first_candidate.epoch_id, second_candidate.epoch_id);
5332
5333        let (first, second) = tokio::join!(
5334            store.initialize_ops_lifecycle_if_absent(&runtime_id, &first_candidate),
5335            store.initialize_ops_lifecycle_if_absent(&runtime_id, &second_candidate),
5336        );
5337        let first = first.unwrap();
5338        let second = second.unwrap();
5339
5340        assert_eq!(first.epoch_id, second.epoch_id);
5341        assert_eq!(
5342            store
5343                .load_ops_lifecycle(&runtime_id)
5344                .await
5345                .unwrap()
5346                .expect("canonical snapshot")
5347                .epoch_id,
5348            first.epoch_id
5349        );
5350    }
5351
5352    #[tokio::test]
5353    async fn unregister_finalization_atomically_retires_ops_epoch_and_is_idempotent() {
5354        let store = InMemoryRuntimeStore::new();
5355        let reopened = store.clone();
5356        let runtime_id = LogicalRuntimeId::new("runtime-unregister-finalization");
5357        let stale_ops = crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new()
5358            .capture_persistence_snapshot(
5359                meerkat_core::RuntimeEpochId::new(),
5360                &meerkat_core::EpochCursorState::new(),
5361            )
5362            .unwrap();
5363        store
5364            .persist_ops_lifecycle(&runtime_id, &stale_ops)
5365            .await
5366            .unwrap();
5367        let retired_ops_epoch = stale_ops.epoch_id.clone();
5368
5369        for _ in 0..2 {
5370            store
5371                .commit_unregister_finalization(
5372                    &runtime_id,
5373                    crate::store::UnregisterFinalizationCommit::new(
5374                        MachineLifecycleCommit::new_with_binding(
5375                            RuntimeState::Stopped,
5376                            MachineLifecycleBindingFacts::new(None, None, None, None),
5377                            crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
5378                        ),
5379                        vec![],
5380                        retired_ops_epoch.clone(),
5381                        crate::meerkat_machine::DeleteOpsFinalizationAuthority::for_store_test(),
5382                    ),
5383                )
5384                .await
5385                .unwrap();
5386        }
5387
5388        assert_eq!(
5389            crate::store::load_runtime_state(&reopened, &runtime_id)
5390                .await
5391                .unwrap(),
5392            Some(RuntimeState::Stopped)
5393        );
5394        assert!(
5395            reopened
5396                .load_ops_lifecycle(&runtime_id)
5397                .await
5398                .unwrap()
5399                .is_none(),
5400            "the same critical section that publishes terminal lifecycle must remove the ops epoch"
5401        );
5402        let late_error = reopened
5403            .persist_ops_lifecycle(&runtime_id, &stale_ops)
5404            .await
5405            .expect_err("a detached callback must not resurrect its retired ops epoch");
5406        assert!(matches!(
5407            late_error,
5408            RuntimeStoreError::OpsLifecycleEpochRetired { epoch_id, .. }
5409                if epoch_id == retired_ops_epoch
5410        ));
5411        assert!(matches!(
5412            reopened
5413                .initialize_ops_lifecycle_if_absent(&runtime_id, &stale_ops)
5414                .await
5415                .expect_err("initialization must honor the same retired-epoch fence"),
5416            RuntimeStoreError::OpsLifecycleEpochRetired { epoch_id, .. }
5417                if epoch_id == retired_ops_epoch
5418        ));
5419        assert!(
5420            reopened
5421                .load_ops_lifecycle(&runtime_id)
5422                .await
5423                .unwrap()
5424                .is_none()
5425        );
5426    }
5427
5428    #[tokio::test]
5429    async fn delayed_old_epoch_finalizer_cannot_delete_or_overwrite_new_ops_epoch() {
5430        let store = InMemoryRuntimeStore::new();
5431        let runtime_id = LogicalRuntimeId::new("runtime-old-finalizer-new-epoch");
5432        let registry = crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new();
5433        let old_ops = registry
5434            .capture_persistence_snapshot(
5435                meerkat_core::RuntimeEpochId::new(),
5436                &meerkat_core::EpochCursorState::new(),
5437            )
5438            .unwrap();
5439        let new_ops = registry
5440            .capture_persistence_snapshot(
5441                meerkat_core::RuntimeEpochId::new(),
5442                &meerkat_core::EpochCursorState::new(),
5443            )
5444            .unwrap();
5445        store
5446            .persist_ops_lifecycle(&runtime_id, &old_ops)
5447            .await
5448            .unwrap();
5449        store
5450            .persist_ops_lifecycle(&runtime_id, &new_ops)
5451            .await
5452            .unwrap();
5453
5454        store
5455            .commit_unregister_finalization(
5456                &runtime_id,
5457                crate::store::UnregisterFinalizationCommit::new(
5458                    MachineLifecycleCommit::new_with_binding(
5459                        RuntimeState::Stopped,
5460                        MachineLifecycleBindingFacts::new(None, None, None, None),
5461                        crate::store::SupervisorAuthoritySnapshot::UnboundNoReceipt,
5462                    ),
5463                    vec![],
5464                    old_ops.epoch_id.clone(),
5465                    crate::meerkat_machine::DeleteOpsFinalizationAuthority::for_store_test(),
5466                ),
5467            )
5468            .await
5469            .unwrap();
5470
5471        assert_eq!(
5472            store
5473                .load_ops_lifecycle(&runtime_id)
5474                .await
5475                .unwrap()
5476                .expect("new epoch row must survive delayed old finalization")
5477                .epoch_id,
5478            new_ops.epoch_id
5479        );
5480        assert!(matches!(
5481            store
5482                .persist_ops_lifecycle(&runtime_id, &old_ops)
5483                .await
5484                .expect_err("retired old epoch stays fenced"),
5485            RuntimeStoreError::OpsLifecycleEpochRetired { .. }
5486        ));
5487        store
5488            .persist_ops_lifecycle(&runtime_id, &new_ops)
5489            .await
5490            .unwrap();
5491    }
5492
5493    #[tokio::test]
5494    async fn clear_session_snapshot_if_current_sets_quarantine_marker_cleared_on_write() {
5495        let store = InMemoryRuntimeStore::new();
5496        let rejected_session = session_with_user("rejected");
5497        let rid = LogicalRuntimeId::for_session(rejected_session.id());
5498        let rejected = serde_json::to_vec(&rejected_session).unwrap();
5499
5500        assert!(!store.is_runtime_projection_quarantined(&rid).await.unwrap());
5501        store
5502            .commit_session_snapshot(
5503                &rid,
5504                SerializedSessionSnapshot {
5505                    session_snapshot: rejected.clone().into(),
5506                },
5507            )
5508            .await
5509            .unwrap();
5510        assert!(
5511            store
5512                .clear_session_snapshot_if_current(&rid, &rejected)
5513                .await
5514                .unwrap()
5515        );
5516        assert!(
5517            store.is_runtime_projection_quarantined(&rid).await.unwrap(),
5518            "clearing the rejected snapshot must record the in-memory quarantine marker"
5519        );
5520
5521        // A live snapshot write reclaims runtime authority and clears the marker.
5522        let mut revived = meerkat_core::Session::with_id(rejected_session.id().clone());
5523        revived.push(meerkat_core::Message::User(
5524            meerkat_core::types::UserMessage::text("revived"),
5525        ));
5526        store
5527            .commit_session_snapshot(
5528                &rid,
5529                SerializedSessionSnapshot {
5530                    session_snapshot: serde_json::to_vec(&revived).unwrap().into(),
5531                },
5532            )
5533            .await
5534            .unwrap();
5535        assert!(
5536            !store.is_runtime_projection_quarantined(&rid).await.unwrap(),
5537            "a live snapshot write must clear the in-memory quarantine marker"
5538        );
5539    }
5540
5541    #[tokio::test]
5542    async fn lifecycle_observation_and_missing_or_version_cas_are_target_local() {
5543        let store = InMemoryRuntimeStore::new();
5544        let runtime_id = LogicalRuntimeId::new("runtime-lifecycle-cas");
5545        let other_runtime_id = LogicalRuntimeId::new("runtime-lifecycle-other");
5546        assert_eq!(
5547            store.observe_machine_lifecycle(&runtime_id).await.unwrap(),
5548            MachineLifecycleObservation::Missing
5549        );
5550
5551        let MachineLifecycleCasOutcome::Applied { version } = store
5552            .compare_and_swap_machine_lifecycle(
5553                &runtime_id,
5554                MachineLifecycleExpectedVersion::Missing,
5555                lifecycle_commit(&runtime_id, RuntimeState::Idle, 7, 3),
5556            )
5557            .await
5558            .unwrap()
5559        else {
5560            panic!("missing row must be inserted");
5561        };
5562        let observed = store.observe_machine_lifecycle(&runtime_id).await.unwrap();
5563        let MachineLifecycleObservation::Decoded {
5564            record,
5565            version: observed_version,
5566        } = &observed
5567        else {
5568            panic!("committed lifecycle row must decode");
5569        };
5570        assert_eq!(observed_version, &version);
5571        assert_eq!(record.runtime_state(), Some(RuntimeState::Idle));
5572        assert_eq!(record.binding().fence_token(), Some(7));
5573
5574        let conflict = store
5575            .compare_and_swap_machine_lifecycle(
5576                &runtime_id,
5577                MachineLifecycleExpectedVersion::Missing,
5578                lifecycle_commit(&runtime_id, RuntimeState::Stopped, 8, 4),
5579            )
5580            .await
5581            .unwrap();
5582        assert_eq!(
5583            conflict,
5584            MachineLifecycleCasOutcome::Conflict {
5585                current: observed.clone()
5586            }
5587        );
5588        assert_eq!(
5589            store
5590                .observe_machine_lifecycle(&other_runtime_id)
5591                .await
5592                .unwrap(),
5593            MachineLifecycleObservation::Missing
5594        );
5595
5596        assert!(matches!(
5597            store
5598                .compare_and_swap_machine_lifecycle(
5599                    &runtime_id,
5600                    MachineLifecycleExpectedVersion::Version(version),
5601                    lifecycle_commit(&runtime_id, RuntimeState::Stopped, 8, 4),
5602                )
5603                .await
5604                .unwrap(),
5605            MachineLifecycleCasOutcome::Applied { .. }
5606        ));
5607    }
5608
5609    #[tokio::test]
5610    async fn malformed_lifecycle_repair_is_blocked_even_with_apparent_highwater() {
5611        let store = InMemoryRuntimeStore::new();
5612        let runtime_id = LogicalRuntimeId::new("runtime-malformed-lifecycle");
5613        let raw = serde_json::to_vec(&serde_json::json!({
5614            "record_version": crate::store::MACHINE_LIFECYCLE_STORE_RECORD_VERSION,
5615            "runtime_state": "idle",
5616            "binding": {
5617                "agent_runtime_id": runtime_id.0.clone(),
5618                "fence_token": 9,
5619                "runtime_generation": 5,
5620                "runtime_epoch_id": "epoch-5"
5621            },
5622            "current_run_id": null,
5623            "pre_run_phase": null,
5624            "unregister_progress": null
5625        }))
5626        .unwrap();
5627        store
5628            .inner
5629            .lock()
5630            .await
5631            .runtime_lifecycle
5632            .insert(runtime_id.0.clone(), raw.clone());
5633
5634        let observed = store.observe_machine_lifecycle(&runtime_id).await.unwrap();
5635        let MachineLifecycleObservation::Malformed { version, .. } = observed else {
5636            panic!("structurally incomplete row must remain malformed evidence");
5637        };
5638        assert!(matches!(
5639            store
5640                .compare_and_swap_machine_lifecycle(
5641                    &runtime_id,
5642                    MachineLifecycleExpectedVersion::Version(version.clone()),
5643                    lifecycle_commit(&runtime_id, RuntimeState::Idle, 8, 5),
5644                )
5645                .await
5646                .expect_err("repair must not lower an independently readable fence"),
5647            RuntimeStoreError::MachineLifecycleRepairBlocked { .. }
5648        ));
5649        assert_eq!(
5650            store
5651                .load_machine_lifecycle_record(&runtime_id)
5652                .await
5653                .unwrap(),
5654            Some(raw.clone())
5655        );
5656
5657        assert!(matches!(
5658            store
5659                .compare_and_swap_machine_lifecycle(
5660                    &runtime_id,
5661                    MachineLifecycleExpectedVersion::Version(version),
5662                    lifecycle_commit(&runtime_id, RuntimeState::Idle, 10, 6),
5663                )
5664                .await
5665                .expect_err("decodable fragments inside malformed bytes are not repair authority"),
5666            RuntimeStoreError::MachineLifecycleRepairBlocked { .. }
5667        ));
5668        assert_eq!(
5669            store
5670                .load_machine_lifecycle_record(&runtime_id)
5671                .await
5672                .unwrap(),
5673            Some(raw)
5674        );
5675    }
5676
5677    #[tokio::test]
5678    async fn malformed_lifecycle_duplicate_highwater_keys_are_repair_blocked() {
5679        let store = InMemoryRuntimeStore::new();
5680        let runtime_id = LogicalRuntimeId::new("runtime-duplicate-lifecycle-fence");
5681        let raw = format!(
5682            r#"{{"record_version":4,"runtime_state":"idle","binding":{{"agent_runtime_id":"{}","fence_token":99,"fence_token":1,"runtime_generation":3,"runtime_epoch_id":"epoch-3"}},"current_run_id":null,"pre_run_phase":null,"supervisor_authority":{{"kind":"unbound_no_receipt"}},"unregister_progress":null}}"#,
5683            runtime_id.0
5684        )
5685        .into_bytes();
5686        store
5687            .inner
5688            .lock()
5689            .await
5690            .runtime_lifecycle
5691            .insert(runtime_id.0.clone(), raw.clone());
5692        let MachineLifecycleObservation::Malformed { version, .. } =
5693            store.observe_machine_lifecycle(&runtime_id).await.unwrap()
5694        else {
5695            panic!("duplicate high-water keys must classify as malformed");
5696        };
5697
5698        assert!(matches!(
5699            store
5700                .compare_and_swap_machine_lifecycle(
5701                    &runtime_id,
5702                    MachineLifecycleExpectedVersion::Version(version),
5703                    lifecycle_commit(&runtime_id, RuntimeState::Idle, 2, 3),
5704                )
5705                .await
5706                .expect_err("ambiguous duplicate high-water must block repair"),
5707            RuntimeStoreError::MachineLifecycleRepairBlocked { .. }
5708        ));
5709        assert_eq!(
5710            store
5711                .load_machine_lifecycle_record(&runtime_id)
5712                .await
5713                .unwrap(),
5714            Some(raw)
5715        );
5716    }
5717
5718    /// Replace one occurrence of `needle` so the fixture differs in content
5719    /// but not in serialized length.
5720    fn splice_bytes(bytes: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
5721        assert_eq!(needle.len(), replacement.len());
5722        let position = bytes
5723            .windows(needle.len())
5724            .position(|window| window == needle)
5725            .expect("fixture needle present");
5726        let mut out = bytes.to_vec();
5727        out[position..position + needle.len()].copy_from_slice(replacement);
5728        out
5729    }
5730
5731    #[tokio::test]
5732    async fn commit_session_snapshot_growth_issues_distinct_store_authority() {
5733        let store = InMemoryRuntimeStore::new();
5734        let mut session = meerkat_core::Session::new();
5735        let rid = LogicalRuntimeId::for_session(session.id());
5736        session.push(meerkat_core::Message::User(
5737            meerkat_core::types::UserMessage::text("first turn".to_string()),
5738        ));
5739        store
5740            .commit_session_snapshot(
5741                &rid,
5742                SerializedSessionSnapshot {
5743                    session_snapshot: serde_json::to_vec(&session).unwrap().into(),
5744                },
5745            )
5746            .await
5747            .unwrap();
5748        let initial_authority = store
5749            .load_whole_blob_store_authority(&rid)
5750            .await
5751            .unwrap()
5752            .unwrap();
5753
5754        session.push(meerkat_core::Message::User(
5755            meerkat_core::types::UserMessage::text("second turn grows the document".to_string()),
5756        ));
5757        let grown = serde_json::to_vec(&session).unwrap();
5758        store
5759            .commit_session_snapshot(
5760                &rid,
5761                SerializedSessionSnapshot {
5762                    session_snapshot: grown.clone().into(),
5763                },
5764            )
5765            .await
5766            .unwrap();
5767
5768        let grown_authority = store
5769            .load_whole_blob_store_authority(&rid)
5770            .await
5771            .unwrap()
5772            .unwrap();
5773        assert_eq!(
5774            grown_authority.store_revision(),
5775            initial_authority.store_revision() + 1
5776        );
5777        assert_ne!(
5778            grown_authority.blob_sha256(),
5779            initial_authority.blob_sha256()
5780        );
5781        assert_eq!(
5782            store.load_session_snapshot(&rid).await.unwrap(),
5783            Some(Arc::new(grown))
5784        );
5785    }
5786
5787    #[tokio::test]
5788    async fn commit_session_snapshot_equal_length_different_bytes_issues_distinct_store_authority()
5789    {
5790        let store = InMemoryRuntimeStore::new();
5791        let mut session = meerkat_core::Session::new();
5792        let rid = LogicalRuntimeId::for_session(session.id());
5793        session.push(meerkat_core::Message::User(
5794            meerkat_core::types::UserMessage::text("probe".to_string()),
5795        ));
5796        session.set_metadata(
5797            "probe_slot",
5798            serde_json::Value::String("probe-fixture-aaaa".to_string()),
5799        );
5800        let first = serde_json::to_vec(&session).unwrap();
5801        let second = splice_bytes(&first, b"probe-fixture-aaaa", b"probe-fixture-bbbb");
5802        assert_eq!(first.len(), second.len());
5803        assert_ne!(first, second);
5804
5805        store
5806            .commit_session_snapshot(
5807                &rid,
5808                SerializedSessionSnapshot {
5809                    session_snapshot: first.into(),
5810                },
5811            )
5812            .await
5813            .unwrap();
5814        let first_authority = store
5815            .load_whole_blob_store_authority(&rid)
5816            .await
5817            .unwrap()
5818            .unwrap();
5819        store
5820            .commit_session_snapshot(
5821                &rid,
5822                SerializedSessionSnapshot {
5823                    session_snapshot: second.clone().into(),
5824                },
5825            )
5826            .await
5827            .unwrap();
5828
5829        let second_authority = store
5830            .load_whole_blob_store_authority(&rid)
5831            .await
5832            .unwrap()
5833            .unwrap();
5834        assert_eq!(
5835            second_authority.store_revision(),
5836            first_authority.store_revision() + 1
5837        );
5838        assert_ne!(
5839            second_authority.blob_sha256(),
5840            first_authority.blob_sha256()
5841        );
5842        assert_eq!(
5843            store.load_session_snapshot(&rid).await.unwrap(),
5844            Some(Arc::new(second)),
5845            "length-equal but different content must mint a distinct store-owned authority"
5846        );
5847    }
5848}