1use 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#[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#[derive(Debug, Default)]
74struct Inner {
75 input_states: HashMap<String, IndexMap<InputId, StoredInputState>>,
77 pending_terminal_owners: HashMap<String, BTreeSet<uuid::Uuid>>,
79 recovery_nonterminal_inputs: HashMap<String, BTreeSet<uuid::Uuid>>,
81 recovery_input_set_revisions: HashMap<String, u64>,
85 input_idempotency_index: HashMap<String, HashMap<String, InputId>>,
87 receipts: HashMap<ReceiptKey, RunBoundaryReceipt>,
89 recovery_boundaries: HashMap<(String, String), super::CommittedRecoveryBoundary>,
91 sessions: HashMap<String, Arc<Vec<u8>>>,
93 session_authorities: HashMap<String, WholeBlobStoreAuthority>,
95 session_catalog: HashMap<String, super::RuntimeSessionCatalogEntry>,
97 whole_blob_provisional_tails: HashMap<String, StoredWholeBlobProvisionalTail>,
99 projection_quarantine: HashSet<String>,
106 runtime_lifecycle: HashMap<String, Vec<u8>>,
110 ops_lifecycle_snapshots: HashMap<String, PersistedOpsSnapshot>,
112 retired_ops_epochs: HashSet<(String, meerkat_core::RuntimeEpochId)>,
115 compaction_projection_outbox:
117 HashMap<String, HashMap<meerkat_core::CompactionProjectionId, CompactionOutboxEntry>>,
118 runtime_delivery_authority: HashMap<String, RuntimeDeliveryAuthorityRecord>,
120 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 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
329fn 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
422fn 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#[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 #[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 #[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 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 #[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(¤t_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 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
1431fn 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
1475fn 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
1498fn 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 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 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 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, ¤t) {
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(¤t)?;
3144 validate_machine_lifecycle_replacement(
3145 ¤t,
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, ¤t) {
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(¤t)?;
3186 validate_machine_lifecycle_replacement(
3187 ¤t,
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 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 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(¤t).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 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 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 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 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 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(¤t).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(¤t).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 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 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}