Skip to main content

meerkat_runtime/meerkat_machine/
mod.rs

1//! MeerkatMachine — session-scoped execution kernel.
2//!
3//! One of two kernels in the Meerkat two-kernel architecture:
4//!
5//! - **MeerkatMachine** (this module) owns session-scoped runtime state:
6//!   input ingress, run lifecycle, completion waiters, async-ops registry,
7//!   comms drain, and tool visibility publication. All mutations flow through
8//!   one unified internal reducer, gated by TLA+-derived precondition guards.
9//!
10//! - **MobMachine** (`meerkat-mob`) owns mob-scoped orchestration: roster,
11//!   flow frames, delegation, and inter-member wiring.
12//!
13//! MeerkatMachine lives in `meerkat-runtime` so `meerkat-session` does not
14//! depend on runtime execution internals. When a session registers a
15//! `CoreExecutor`, a background `RuntimeLoop` task is spawned. Input acceptance
16//! queues through the driver; wake signals the loop; the loop dequeues, stages,
17//! applies via `CoreExecutor`, and marks inputs consumed.
18
19use std::collections::{BTreeSet, HashMap, HashSet};
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::Arc;
23use std::sync::RwLock as StdRwLock;
24#[cfg(not(target_arch = "wasm32"))]
25use std::sync::{Mutex as StdMutex, OnceLock, Weak};
26
27use meerkat_core::lifecycle::{InputId, RunId};
28use meerkat_core::time_compat::Instant;
29use meerkat_core::tool_scope::ToolScopeTurnOverlay;
30use meerkat_core::types::SessionId;
31use meerkat_core::{BlobId, BlobPayload, BlobRef, BlobStore, BlobStoreError};
32use meerkat_core::{
33    DeferredToolLoadAuthority, SessionToolVisibilityState, ToolFilter, ToolScopeApplyError,
34    ToolScopeRevision, ToolScopeStageError, ToolVisibilityOwner, ToolVisibilityWitness,
35};
36
37use crate::accept::AcceptOutcome;
38use crate::driver::ephemeral::EphemeralRuntimeDriver;
39use crate::driver::persistent::PersistentRuntimeDriver;
40use crate::identifiers::LogicalRuntimeId;
41use crate::input::Input;
42use crate::input_state::{
43    InputAbandonReason, InputLifecycleState, InputStateSeed, InputTerminalOutcome,
44};
45use crate::meerkat_machine_types::{
46    HydratedSessionLlmState, MeerkatAdmittedInputSnapshot, MeerkatArchiveSnapshot,
47    MeerkatBindingSnapshot, MeerkatCompletionWaiterSnapshot, MeerkatCompletionWaitersSnapshot,
48    MeerkatControlSnapshot, MeerkatCursorSnapshot, MeerkatDrainSnapshot, MeerkatDriverKind,
49    MeerkatFormalStateProjection, MeerkatInputsSnapshot, MeerkatLedgerSnapshot,
50    MeerkatMachineCommand, MeerkatMachineCommandError, MeerkatMachineCommandResult,
51    MeerkatMachineRunFailure, MeerkatMachineSpineSnapshot, MeerkatOpsSnapshot,
52    MemberResidencyExpectation, SessionLlmCapabilityDelta, SessionLlmCapabilitySurface,
53    SessionLlmReconfigureHost, SessionLlmReconfigureReport, SessionLlmReconfigureRequest,
54    SessionToolVisibilityDelta,
55};
56use crate::runtime_state::RuntimeState;
57use crate::service_ext::SessionServiceRuntimeExt;
58use crate::store::RuntimeStore;
59use crate::tokio;
60use crate::tokio::sync::{Mutex, RwLock, mpsc};
61#[cfg(test)]
62use crate::traits::RuntimeDriver;
63use crate::traits::{
64    DestroyReport, RecoveryReport, RecycleReport, ResetReport, RetireReport,
65    RuntimeControlPlaneError, RuntimeDriverError,
66};
67
68#[allow(clippy::expect_used)]
69pub(crate) fn recover_projected_authority(
70    state: dsl::MeerkatMachineState,
71    context: &'static str,
72) -> dsl::MeerkatMachineAuthority {
73    dsl::MeerkatMachineAuthority::recover_from_state(state).expect(context)
74}
75
76struct ToolVisibilityOwnerGeneratedAuthorityBridgeToken;
77
78static TOOL_VISIBILITY_OWNER_GENERATED_AUTHORITY_BRIDGE_TOKEN:
79    ToolVisibilityOwnerGeneratedAuthorityBridgeToken =
80    ToolVisibilityOwnerGeneratedAuthorityBridgeToken;
81
82fn tool_visibility_owner_generated_authority_bridge_token()
83-> &'static (dyn std::any::Any + Send + Sync) {
84    &TOOL_VISIBILITY_OWNER_GENERATED_AUTHORITY_BRIDGE_TOKEN
85}
86
87#[doc(hidden)]
88#[allow(improper_ctypes_definitions, unsafe_code)]
89#[unsafe(export_name = concat!(
90    "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_tool_visibility_owner_",
91    env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
92))]
93pub extern "Rust" fn tool_visibility_owner_generated_authority_bridge_token_is_valid(
94    token: &(dyn std::any::Any + Send + Sync),
95) -> bool {
96    token.is::<ToolVisibilityOwnerGeneratedAuthorityBridgeToken>()
97}
98
99fn generated_tool_visibility_owner(
100    owner: Arc<dyn ToolVisibilityOwner>,
101) -> Result<meerkat_core::GeneratedToolVisibilityOwner, String> {
102    #[allow(improper_ctypes_definitions, unsafe_code)]
103    unsafe extern "Rust" {
104        #[link_name = concat!(
105            "__meerkat_core_runtime_generated_tool_visibility_owner_build_v1_",
106            env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
107        )]
108        fn core_runtime_generated_tool_visibility_owner_build(
109            token: &'static (dyn std::any::Any + Send + Sync),
110            owner: Arc<dyn ToolVisibilityOwner>,
111        ) -> Result<meerkat_core::GeneratedToolVisibilityOwner, String>;
112    }
113    #[allow(unsafe_code)]
114    unsafe {
115        core_runtime_generated_tool_visibility_owner_build(
116            tool_visibility_owner_generated_authority_bridge_token(),
117            owner,
118        )
119    }
120}
121
122/// Shared generated authorities for a standalone facade session.
123///
124/// Standalone has no runtime loop, but turn recovery, model routing, and tool
125/// visibility still describe one session machine. Keeping all three handles on
126/// the same authority is required for sticky fallback: the turn handle admits
127/// ErrorRecovery and the routing handle commits identity + visibility against
128/// that exact state.
129#[derive(Clone)]
130pub struct StandaloneSessionRuntimeAuthorities {
131    tool_visibility_owner: meerkat_core::GeneratedToolVisibilityOwner,
132    turn_state: Arc<dyn meerkat_core::TurnStateHandle>,
133    model_routing: Arc<dyn meerkat_core::handles::ModelRoutingHandle>,
134    #[cfg(test)]
135    model_routing_test: Arc<crate::handles::RuntimeModelRoutingHandle>,
136}
137
138impl StandaloneSessionRuntimeAuthorities {
139    pub fn tool_visibility_owner(&self) -> &meerkat_core::GeneratedToolVisibilityOwner {
140        &self.tool_visibility_owner
141    }
142
143    pub fn turn_state(&self) -> &Arc<dyn meerkat_core::TurnStateHandle> {
144        &self.turn_state
145    }
146
147    pub fn model_routing(&self) -> &Arc<dyn meerkat_core::handles::ModelRoutingHandle> {
148        &self.model_routing
149    }
150
151    #[cfg(test)]
152    pub(crate) fn commit_sticky_model_fallback_for_test(
153        &self,
154        previous_identity: &meerkat_core::SessionLlmIdentity,
155        target_identity: &meerkat_core::SessionLlmIdentity,
156        target_profile: &meerkat_core::ModelProfileWitness,
157        visibility_plan: &meerkat_core::handles::StickyModelFallbackVisibilityPlan,
158        retry_attempt: u32,
159    ) -> Result<(), meerkat_core::handles::DslTransitionError> {
160        self.model_routing_test
161            .commit_sticky_model_fallback_for_test(
162                previous_identity,
163                target_identity,
164                target_profile,
165                visibility_plan,
166                retry_attempt,
167            )
168    }
169}
170
171/// Build the shared generated authority bundle for a standalone session.
172///
173/// Standalone sessions do not have a runtime loop, but durable tool visibility
174/// and sticky model fallback are still machine facts. This bundle gives those
175/// sessions the same single-authority path used by runtime-backed sessions.
176pub fn standalone_session_runtime_authorities(
177    session_id: &SessionId,
178    current_identity: &meerkat_core::SessionLlmIdentity,
179    model_profile: Option<&meerkat_core::model_profile::ModelProfile>,
180    capability_base_filter: &ToolFilter,
181) -> Result<StandaloneSessionRuntimeAuthorities, String> {
182    let mut authority = dsl_authority::new_registered_authority(session_id)
183        .map_err(|err| dsl_authority::map_error(err, "standalone visibility authority"))?;
184    let (current_capability_surface, current_capability_surface_status) = match model_profile {
185        Some(profile) => (
186            Some(dsl::SessionLlmCapabilitySurface {
187                supports_temperature: profile.supports_temperature,
188                supports_thinking: profile.supports_thinking,
189                supports_reasoning: profile.supports_reasoning,
190                inline_video: profile.inline_video,
191                vision: profile.vision,
192                image_input: profile.image_input,
193                image_tool_results: profile.image_tool_results,
194                supports_web_search: profile.supports_web_search,
195                image_generation: profile.image_generation,
196                realtime: profile.realtime,
197                call_timeout_secs: profile.call_timeout_secs,
198            }),
199            dsl::SessionLlmCapabilitySurfaceStatus::Resolved,
200        ),
201        None => (None, dsl::SessionLlmCapabilitySurfaceStatus::Unresolved),
202    };
203    dsl::MeerkatMachineMutator::apply(
204        &mut authority,
205        dsl::MeerkatMachineInput::HydrateSessionLlmState {
206            current_identity: dsl::SessionLlmIdentity::from_domain(current_identity),
207            current_capability_surface,
208            current_capability_surface_status,
209            current_capability_base_filter: dsl::ToolFilter::from_domain(capability_base_filter),
210        },
211    )
212    .map_err(|err| dsl_authority::map_error(err, "standalone visibility hydration"))?;
213    dsl::MeerkatMachineMutator::apply(
214        &mut authority,
215        dsl::MeerkatMachineInput::SetModelRoutingBaseline {
216            baseline_model: current_identity.model.clone(),
217            realtime_capable: model_profile.is_some_and(|profile| profile.realtime),
218        },
219    )
220    .map_err(|err| dsl_authority::map_error(err, "standalone model routing baseline"))?;
221    let authority = Arc::new(std::sync::Mutex::new(authority));
222    let owner = Arc::new(MachineToolVisibilityOwner::new());
223    owner.bind_dsl_authority(Arc::clone(&authority));
224    let shared_handle_authority = Arc::new(crate::handles::HandleDslAuthority::from_shared(
225        Arc::clone(&authority),
226    ));
227    let tool_visibility_owner =
228        generated_tool_visibility_owner(Arc::clone(&owner) as Arc<dyn ToolVisibilityOwner>)?;
229    let turn_state = Arc::new(crate::handles::RuntimeTurnStateHandle::standalone(
230        Arc::clone(&shared_handle_authority),
231        session_id.clone(),
232    )) as Arc<dyn meerkat_core::TurnStateHandle>;
233    let runtime_model_routing = Arc::new(
234        crate::handles::RuntimeModelRoutingHandle::new_with_visibility_owner(
235            shared_handle_authority,
236            owner,
237        ),
238    );
239    let model_routing =
240        Arc::clone(&runtime_model_routing) as Arc<dyn meerkat_core::handles::ModelRoutingHandle>;
241    Ok(StandaloneSessionRuntimeAuthorities {
242        tool_visibility_owner,
243        turn_state,
244        model_routing,
245        #[cfg(test)]
246        model_routing_test: runtime_model_routing,
247    })
248}
249
250/// Build only the standalone visibility projection for tool-only hosts.
251/// AgentFactory uses [`standalone_session_runtime_authorities`] so turn,
252/// routing, and visibility never split across private authorities.
253pub fn standalone_tool_visibility_owner(
254    session_id: &SessionId,
255    current_identity: &meerkat_core::SessionLlmIdentity,
256    model_profile: Option<&meerkat_core::model_profile::ModelProfile>,
257    capability_base_filter: &ToolFilter,
258) -> Result<meerkat_core::GeneratedToolVisibilityOwner, String> {
259    standalone_session_runtime_authorities(
260        session_id,
261        current_identity,
262        model_profile,
263        capability_base_filter,
264    )
265    .map(|authorities| authorities.tool_visibility_owner)
266}
267
268/// Error type for [`MeerkatMachine::prepare_bindings`].
269#[derive(Debug, thiserror::Error)]
270pub enum RuntimeBindingsError {
271    /// Session was not found after registration (should not happen in practice).
272    #[error("session {0} not found in runtime adapter after registration")]
273    SessionNotFound(SessionId),
274    /// Machine-owned binding preparation failed before bindings were published.
275    #[error("failed to prepare runtime bindings for session {0}: {1}")]
276    PrepareFailed(SessionId, String),
277}
278
279/// Generated public projection for an input-state seed.
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub struct InputPublicStateProjection {
282    pub lifecycle_state: dsl::InputPublicLifecycleState,
283    pub terminal_outcome: Option<dsl::InputPublicTerminalOutcome>,
284}
285
286/// Runtime lifecycle/admission facts emitted by generated MeerkatMachine
287/// authority for a public runtime-state projection.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub struct RuntimeLifecycleFacts {
290    pub terminality: dsl::RuntimeLifecycleTerminality,
291    pub input_admission: dsl::RuntimeInputAdmission,
292    pub queue_admission: dsl::RuntimeQueueAdmission,
293    pub prepare_admission: dsl::RuntimePrepareAdmission,
294    pub ingress_admission: dsl::RuntimeIngressAdmission,
295}
296
297impl RuntimeLifecycleFacts {
298    #[must_use]
299    pub fn can_accept_input(self) -> bool {
300        self.input_admission == dsl::RuntimeInputAdmission::AcceptsInput
301    }
302
303    #[must_use]
304    pub fn can_process_queue(self) -> bool {
305        self.queue_admission == dsl::RuntimeQueueAdmission::ProcessesQueue
306    }
307
308    #[must_use]
309    pub fn can_prepare_run(self) -> bool {
310        self.prepare_admission == dsl::RuntimePrepareAdmission::Ready
311    }
312
313    #[must_use]
314    pub fn is_terminal(self) -> bool {
315        self.terminality == dsl::RuntimeLifecycleTerminality::Terminal
316    }
317}
318
319/// Runtime-loop queue-drain admission feedback emitted by generated
320/// MeerkatMachine authority.
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub struct RuntimeLoopQueueAdmissionPlan {
323    pub queue_admission: dsl::RuntimeQueueAdmission,
324    pub run_binding: dsl::RuntimeLoopRunBinding,
325}
326
327impl RuntimeLoopQueueAdmissionPlan {
328    #[must_use]
329    pub fn can_process_queue(self) -> bool {
330        self.queue_admission == dsl::RuntimeQueueAdmission::ProcessesQueue
331    }
332
333    #[must_use]
334    pub fn uses_prebound_run(self) -> bool {
335        self.run_binding == dsl::RuntimeLoopRunBinding::UsePrebound
336    }
337}
338
339/// Classify runtime lifecycle/admission facts through generated
340/// MeerkatMachine authority. Callers provide only the observed state variant;
341/// all behavior-affecting facts come back as generated typed feedback.
342pub fn classify_runtime_lifecycle_state(
343    state: RuntimeState,
344) -> Result<RuntimeLifecycleFacts, String> {
345    let observed_state = dsl_authority::observed_runtime_lifecycle_state(state);
346    let mut authority = projection_authority();
347    let transition = dsl::MeerkatMachineMutator::apply(
348        &mut authority,
349        dsl::MeerkatMachineInput::ClassifyRuntimeLifecycleState {
350            state: observed_state,
351        },
352    )
353    .map_err(|err| {
354        format!("MeerkatMachine rejected runtime lifecycle classification for {state}: {err}")
355    })?;
356
357    transition
358        .into_effects()
359        .into_iter()
360        .find_map(|effect| match effect {
361            dsl::MeerkatMachineEffect::RuntimeLifecycleStateClassified {
362                state,
363                terminality,
364                input_admission,
365                queue_admission,
366                prepare_admission,
367                ingress_admission,
368            } if state == observed_state => Some(RuntimeLifecycleFacts {
369                terminality,
370                input_admission,
371                queue_admission,
372                prepare_admission,
373                ingress_admission,
374            }),
375            _ => None,
376        })
377        .ok_or_else(|| {
378            format!("MeerkatMachine emitted no runtime lifecycle classification for {state}")
379        })
380}
381
382/// Classify the store-visible durable runtime lifecycle state through
383/// generated MeerkatMachine authority. The caller supplies only the live
384/// observed state; generated feedback decides the recovery projection.
385pub fn classify_runtime_lifecycle_durable_state(
386    state: RuntimeState,
387) -> Result<RuntimeState, String> {
388    classify_runtime_lifecycle_durable_state_with_pre_run_phase(state, None)
389}
390
391/// Classify the durable lifecycle while retaining the live run's coarse
392/// pre-run phase. Running itself is process-local, but a run admitted from a
393/// retired runtime must remain durably retired after cold recovery.
394pub(crate) fn classify_runtime_lifecycle_durable_state_with_pre_run_phase(
395    state: RuntimeState,
396    pre_run_phase: Option<RuntimeState>,
397) -> Result<RuntimeState, String> {
398    let observed_state = dsl_authority::observed_runtime_lifecycle_state(state);
399    let pre_run_phase = pre_run_phase.and_then(dsl_authority::pre_run_phase_from_runtime_state);
400    let mut authority = projection_authority();
401    let transition = dsl::MeerkatMachineMutator::apply(
402        &mut authority,
403        dsl::MeerkatMachineInput::ClassifyRuntimeLifecycleDurability {
404            state: observed_state,
405            pre_run_phase,
406        },
407    )
408    .map_err(|err| {
409        format!(
410            "MeerkatMachine rejected runtime lifecycle durability classification for {state}: {err}"
411        )
412    })?;
413
414    transition
415        .into_effects()
416        .into_iter()
417        .find_map(|effect| match effect {
418            dsl::MeerkatMachineEffect::RuntimeLifecycleDurabilityClassified {
419                state,
420                durable_state,
421            } if state == observed_state => Some(
422                dsl_authority::runtime_state_from_observed_lifecycle_state(durable_state),
423            ),
424            _ => None,
425        })
426        .ok_or_else(|| {
427            format!(
428                "MeerkatMachine emitted no runtime lifecycle durability classification for {state}"
429            )
430        })
431}
432
433/// Classify runtime-loop queue admission through generated MeerkatMachine
434/// authority. The caller provides the observed runtime state and the structural
435/// fact that a current run id is bound; generated feedback decides whether the
436/// queue may drain and whether that bound run id must be reused.
437pub fn classify_runtime_loop_queue_admission(
438    state: RuntimeState,
439    current_run_bound: bool,
440) -> Result<RuntimeLoopQueueAdmissionPlan, String> {
441    let observed_state = dsl_authority::observed_runtime_lifecycle_state(state);
442    let mut authority = projection_authority();
443    let transition = dsl::MeerkatMachineMutator::apply(
444        &mut authority,
445        dsl::MeerkatMachineInput::ClassifyRuntimeLoopQueueAdmission {
446            state: observed_state,
447            current_run_bound,
448        },
449    )
450    .map_err(|err| {
451        format!(
452            "MeerkatMachine rejected runtime-loop queue admission for {state} with current_run_bound={current_run_bound}: {err}"
453        )
454    })?;
455
456    transition
457        .into_effects()
458        .into_iter()
459        .find_map(|effect| match effect {
460            dsl::MeerkatMachineEffect::RuntimeLoopQueueAdmissionClassified {
461                state,
462                current_run_bound: observed_current_run_bound,
463                queue_admission,
464                run_binding,
465            } if state == observed_state && observed_current_run_bound == current_run_bound => {
466                Some(RuntimeLoopQueueAdmissionPlan {
467                    queue_admission,
468                    run_binding,
469                })
470            }
471            _ => None,
472        })
473        .ok_or_else(|| {
474            format!(
475                "MeerkatMachine emitted no runtime-loop queue admission for {state} with current_run_bound={current_run_bound}"
476            )
477        })
478}
479
480/// Machine-owned arbitration verdict between the live DSL lifecycle phase and
481/// the durable control projection, emitted by generated MeerkatMachine
482/// authority. `publish_control` is the terminal-precedence decision (the
483/// published control projection supersedes the live DSL phase);
484/// `selected_raw_phase` is the chosen phase without the visibility rewrite;
485/// `visible_phase` is the externally-visible phase after the
486/// Running+pre_run(Retired)->Retired rewrite. The shell mirrors all three.
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub struct VisibleRuntimePhasePlan {
489    pub publish_control: bool,
490    pub selected_raw_phase: RuntimeState,
491    pub visible_phase: RuntimeState,
492}
493
494/// Resolve the authoritative/visible runtime phase through generated
495/// MeerkatMachine authority. The shell feeds only the five pure
496/// [`RuntimeState`] observations it already holds; the machine owns BOTH the
497/// terminal-precedence `publish_control` policy AND the
498/// Running+pre_run(Retired)->Retired visibility rewrite. The shell mirrors the
499/// emitted verdict and re-derives nothing, failing closed if no verdict is
500/// emitted.
501pub fn resolve_visible_runtime_phase(
502    dsl_phase: RuntimeState,
503    dsl_pre_run_phase: Option<RuntimeState>,
504    control_phase: RuntimeState,
505    control_pre_run_phase: Option<RuntimeState>,
506    has_runtime_persistence: bool,
507) -> Result<VisibleRuntimePhasePlan, String> {
508    let observed_dsl = dsl_authority::observed_runtime_lifecycle_state(dsl_phase);
509    let observed_control = dsl_authority::observed_runtime_lifecycle_state(control_phase);
510    let observed_dsl_pre_run =
511        dsl_pre_run_phase.map(dsl_authority::observed_runtime_lifecycle_state);
512    let observed_control_pre_run =
513        control_pre_run_phase.map(dsl_authority::observed_runtime_lifecycle_state);
514    let mut authority = projection_authority();
515    let transition = dsl::MeerkatMachineMutator::apply(
516        &mut authority,
517        dsl::MeerkatMachineInput::ResolveVisibleRuntimePhase {
518            dsl_phase: observed_dsl,
519            dsl_pre_run_phase: observed_dsl_pre_run,
520            control_phase: observed_control,
521            control_pre_run_phase: observed_control_pre_run,
522            has_runtime_persistence,
523        },
524    )
525    .map_err(|err| {
526        format!(
527            "MeerkatMachine rejected visible runtime phase resolution \
528             (dsl={dsl_phase}, control={control_phase}, persistence={has_runtime_persistence}): {err}"
529        )
530    })?;
531
532    transition
533        .into_effects()
534        .into_iter()
535        .find_map(|effect| match effect {
536            dsl::MeerkatMachineEffect::VisibleRuntimePhaseResolved {
537                publish_control,
538                selected_raw_phase,
539                visible_phase,
540            } => Some(VisibleRuntimePhasePlan {
541                publish_control,
542                selected_raw_phase: dsl_authority::runtime_state_from_observed_lifecycle_state(
543                    selected_raw_phase,
544                ),
545                visible_phase: dsl_authority::runtime_state_from_observed_lifecycle_state(
546                    visible_phase,
547                ),
548            }),
549            _ => None,
550        })
551        .ok_or_else(|| {
552            format!(
553                "MeerkatMachine emitted no visible runtime phase resolution \
554                 (dsl={dsl_phase}, control={control_phase}, persistence={has_runtime_persistence})"
555            )
556        })
557}
558
559/// Resolve the public lifecycle class for a machine-derived input phase
560/// through generated MeerkatMachine authority.
561pub fn resolve_input_public_lifecycle_projection(
562    input_id: &InputId,
563    phase: InputLifecycleState,
564) -> Result<dsl::InputPublicLifecycleState, String> {
565    let input_key = input_id.to_string();
566    let mut authority = projection_authority();
567    let transition = dsl::MeerkatMachineMutator::apply(
568        &mut authority,
569        dsl::MeerkatMachineInput::ResolveInputPublicLifecycle {
570            input_id: input_key.clone(),
571            phase: observed_input_phase(phase),
572        },
573    )
574    .map_err(|err| {
575        format!("MeerkatMachine rejected public lifecycle projection for '{input_id}': {err}")
576    })?;
577
578    transition
579        .into_effects()
580        .into_iter()
581        .find_map(|effect| match effect {
582            dsl::MeerkatMachineEffect::InputPublicLifecycleResolved { input_id, phase }
583                if input_id == input_key =>
584            {
585                Some(phase)
586            }
587            _ => None,
588        })
589        .ok_or_else(|| {
590            format!("MeerkatMachine emitted no public lifecycle projection for '{input_id}'")
591        })
592}
593
594/// Resolve public lifecycle and terminal result classes for a machine-derived
595/// input-state seed through generated MeerkatMachine authority.
596pub fn resolve_input_public_state_projection(
597    input_id: &InputId,
598    seed: &InputStateSeed,
599) -> Result<InputPublicStateProjection, String> {
600    let lifecycle_state = resolve_input_public_lifecycle_projection(input_id, seed.phase)?;
601    let terminal_outcome = resolve_input_public_terminal_projection(input_id, seed)?;
602    Ok(InputPublicStateProjection {
603        lifecycle_state,
604        terminal_outcome,
605    })
606}
607
608pub(crate) fn input_seed_behavioral_terminality_via_authority(
609    input_id: &InputId,
610    seed: &InputStateSeed,
611) -> Result<bool, String> {
612    classify_input_behavioral_terminality(input_id, seed.phase, seed.terminal_outcome.as_ref())
613}
614
615pub(crate) fn input_phase_behavioral_terminality_via_authority(
616    input_id: &InputId,
617    phase: InputLifecycleState,
618    terminal_outcome: Option<InputTerminalOutcome>,
619) -> Result<bool, String> {
620    classify_input_behavioral_terminality(input_id, phase, terminal_outcome.as_ref())
621}
622
623/// Authorize DSL-owned input-state seed facts before they are written to a
624/// runtime store.
625pub(crate) fn authorize_stored_input_state_seed(
626    input_id: &InputId,
627    seed: &InputStateSeed,
628) -> Result<(), String> {
629    let input_key = input_id.to_string();
630    let (terminal_kind, superseded_by, aggregate_id, abandon_reason, abandon_attempt_count) =
631        input_seed_terminal_parts(seed)?;
632    let mut authority = projection_authority();
633    let transition = dsl::MeerkatMachineMutator::apply(
634        &mut authority,
635        dsl::MeerkatMachineInput::AuthorizeStoredInputStateSeed {
636            input_id: input_key.clone(),
637            phase: observed_input_phase(seed.phase),
638            terminal_kind,
639            superseded_by,
640            aggregate_id,
641            abandon_reason,
642            abandon_attempt_count,
643            attempt_count: u64::from(seed.attempt_count),
644            run_id: seed.last_run_id.as_ref().map(dsl::RunId::from_domain),
645            boundary_sequence: seed.last_boundary_sequence,
646            admission_sequence: seed.admission_sequence,
647            recovery_lane: seed.recovery_lane.map(dsl::InputLane::from),
648        },
649    )
650    .map_err(|err| {
651        format!("MeerkatMachine rejected stored input-state seed for '{input_id}': {err}")
652    })?;
653
654    transition
655        .into_effects()
656        .into_iter()
657        .find_map(|effect| match effect {
658            dsl::MeerkatMachineEffect::StoredInputStateSeedAuthorized { input_id }
659                if input_id == input_key =>
660            {
661                Some(())
662            }
663            _ => None,
664        })
665        .ok_or_else(|| {
666            format!("MeerkatMachine emitted no stored input-state seed authority for '{input_id}'")
667        })
668}
669
670fn classify_input_behavioral_terminality(
671    input_id: &InputId,
672    phase: InputLifecycleState,
673    terminal_outcome: Option<&InputTerminalOutcome>,
674) -> Result<bool, String> {
675    let input_key = input_id.to_string();
676    let (terminal_kind, abandon_reason) = input_terminality_parts(terminal_outcome);
677    let mut authority = projection_authority();
678    let transition = dsl::MeerkatMachineMutator::apply(
679        &mut authority,
680        dsl::MeerkatMachineInput::ClassifyInputTerminality {
681            input_id: input_key.clone(),
682            phase: observed_input_phase(phase),
683            terminal_kind,
684            abandon_reason,
685        },
686    )
687    .map_err(|err| {
688        format!("MeerkatMachine rejected behavioral input terminality for '{input_id}': {err}")
689    })?;
690
691    let mut terminality = None;
692    for effect in transition.into_effects() {
693        match effect {
694            dsl::MeerkatMachineEffect::InputBehavioralTerminalityResolved {
695                input_id,
696                terminal,
697            } if input_id == input_key => terminality = Some(terminal),
698            other => {
699                return Err(format!(
700                    "MeerkatMachine emitted unexpected behavioral input terminality effect for '{input_id}': {other:?}"
701                ));
702            }
703        }
704    }
705    terminality.ok_or_else(|| {
706        format!("MeerkatMachine emitted no behavioral input terminality for '{input_id}'")
707    })
708}
709
710fn resolve_input_public_terminal_projection(
711    input_id: &InputId,
712    seed: &InputStateSeed,
713) -> Result<Option<dsl::InputPublicTerminalOutcome>, String> {
714    let input_key = input_id.to_string();
715    let (terminal_kind, abandon_reason) = input_terminality_parts(seed.terminal_outcome.as_ref());
716    let mut authority = projection_authority();
717    let transition = dsl::MeerkatMachineMutator::apply(
718        &mut authority,
719        dsl::MeerkatMachineInput::ResolveInputPublicTerminalOutcome {
720            input_id: input_key.clone(),
721            phase: observed_input_phase(seed.phase),
722            terminal_kind,
723            abandon_reason,
724        },
725    )
726    .map_err(|err| {
727        format!("MeerkatMachine rejected public terminal projection for '{input_id}': {err}")
728    })?;
729
730    transition
731        .into_effects()
732        .into_iter()
733        .find_map(|effect| match effect {
734            dsl::MeerkatMachineEffect::InputPublicTerminalOutcomeResolved {
735                input_id,
736                terminal_outcome,
737            } if input_id == input_key => Some(terminal_outcome),
738            _ => None,
739        })
740        .ok_or_else(|| {
741            format!("MeerkatMachine emitted no public terminal projection for '{input_id}'")
742        })
743}
744
745fn projection_authority() -> dsl::MeerkatMachineAuthority {
746    dsl_authority::new_initialized_authority("projection authority must initialize")
747}
748
749#[cfg(feature = "live")]
750fn live_unbound_rejection_authority() -> crate::driver::ephemeral::SharedIngressDslAuthority {
751    Arc::new(std::sync::Mutex::new(
752        dsl_authority::new_initialized_authority(
753            "live unbound rejection authority must initialize",
754        ),
755    ))
756}
757
758fn observed_input_phase(phase: InputLifecycleState) -> dsl::RecoveredInputObservedPhase {
759    match phase {
760        InputLifecycleState::Accepted => dsl::RecoveredInputObservedPhase::Accepted,
761        InputLifecycleState::Queued => dsl::RecoveredInputObservedPhase::Queued,
762        InputLifecycleState::Staged => dsl::RecoveredInputObservedPhase::Staged,
763        InputLifecycleState::Applied => dsl::RecoveredInputObservedPhase::Applied,
764        InputLifecycleState::AppliedPendingConsumption => {
765            dsl::RecoveredInputObservedPhase::AppliedPendingConsumption
766        }
767        InputLifecycleState::Consumed => dsl::RecoveredInputObservedPhase::Consumed,
768        InputLifecycleState::Superseded => dsl::RecoveredInputObservedPhase::Superseded,
769        InputLifecycleState::Coalesced => dsl::RecoveredInputObservedPhase::Coalesced,
770        InputLifecycleState::Abandoned => dsl::RecoveredInputObservedPhase::Abandoned,
771    }
772}
773
774type InputSeedTerminalParts = (
775    Option<dsl::InputTerminalKind>,
776    Option<String>,
777    Option<String>,
778    Option<dsl::InputAbandonReason>,
779    u64,
780);
781
782fn input_seed_terminal_parts(seed: &InputStateSeed) -> Result<InputSeedTerminalParts, String> {
783    match seed.terminal_outcome.as_ref() {
784        None => Ok((None, None, None, None, 0)),
785        Some(InputTerminalOutcome::Consumed) => {
786            Ok((Some(dsl::InputTerminalKind::Consumed), None, None, None, 0))
787        }
788        Some(InputTerminalOutcome::Superseded { superseded_by }) => Ok((
789            Some(dsl::InputTerminalKind::Superseded),
790            Some(superseded_by.to_string()),
791            None,
792            None,
793            0,
794        )),
795        Some(InputTerminalOutcome::Coalesced { aggregate_id }) => Ok((
796            Some(dsl::InputTerminalKind::Coalesced),
797            None,
798            Some(aggregate_id.to_string()),
799            None,
800            0,
801        )),
802        Some(InputTerminalOutcome::Abandoned { reason }) => {
803            let abandon_attempt_count = match reason {
804                InputAbandonReason::MaxAttemptsExhausted { attempts } => u64::from(*attempts),
805                _ => u64::from(seed.attempt_count),
806            };
807            Ok((
808                Some(dsl::InputTerminalKind::Abandoned),
809                None,
810                None,
811                input_terminality_parts(seed.terminal_outcome.as_ref()).1,
812                abandon_attempt_count,
813            ))
814        }
815    }
816}
817
818fn input_terminality_parts(
819    outcome: Option<&InputTerminalOutcome>,
820) -> (
821    Option<dsl::InputTerminalKind>,
822    Option<dsl::InputAbandonReason>,
823) {
824    match outcome {
825        None => (None, None),
826        Some(InputTerminalOutcome::Consumed) => (Some(dsl::InputTerminalKind::Consumed), None),
827        Some(InputTerminalOutcome::Superseded { .. }) => {
828            (Some(dsl::InputTerminalKind::Superseded), None)
829        }
830        Some(InputTerminalOutcome::Coalesced { .. }) => {
831            (Some(dsl::InputTerminalKind::Coalesced), None)
832        }
833        Some(InputTerminalOutcome::Abandoned { reason }) => (
834            Some(dsl::InputTerminalKind::Abandoned),
835            Some(match reason {
836                InputAbandonReason::Retired => dsl::InputAbandonReason::Retired,
837                InputAbandonReason::Reset => dsl::InputAbandonReason::Reset,
838                InputAbandonReason::Stopped => dsl::InputAbandonReason::Stopped,
839                InputAbandonReason::Destroyed => dsl::InputAbandonReason::Destroyed,
840                InputAbandonReason::Cancelled => dsl::InputAbandonReason::Cancelled,
841                InputAbandonReason::MaxAttemptsExhausted { .. } => {
842                    dsl::InputAbandonReason::MaxAttemptsExhausted
843                }
844            }),
845        ),
846    }
847}
848
849#[derive(Debug, Default)]
850struct UnavailableBlobStore;
851
852impl UnavailableBlobStore {
853    fn error() -> BlobStoreError {
854        BlobStoreError::Unsupported(
855            "persistent runtime constructed without blob store; blob-backed inputs require a BlobStore"
856                .to_string(),
857        )
858    }
859}
860
861#[cfg(not(target_arch = "wasm32"))]
862struct PersistentAuthAuthorityBundle {
863    store: StdMutex<Weak<dyn RuntimeStore>>,
864    auth_lease: Arc<crate::handles::RuntimeAuthLeaseHandle>,
865    oauth_flows: Arc<crate::handles::RuntimeOAuthFlowHandle>,
866}
867
868#[cfg(not(target_arch = "wasm32"))]
869#[derive(Debug, Clone, PartialEq, Eq, Hash)]
870enum PersistentAuthAuthorityKey {
871    Durable(String),
872    Process(usize),
873}
874
875#[cfg(not(target_arch = "wasm32"))]
876static PERSISTENT_AUTH_AUTHORITIES: OnceLock<
877    StdMutex<HashMap<PersistentAuthAuthorityKey, Arc<PersistentAuthAuthorityBundle>>>,
878> = OnceLock::new();
879
880#[cfg(not(target_arch = "wasm32"))]
881fn runtime_store_identity(store: &Arc<dyn RuntimeStore>) -> PersistentAuthAuthorityKey {
882    store
883        .auth_authority_key()
884        .map(PersistentAuthAuthorityKey::Durable)
885        .unwrap_or_else(|| {
886            PersistentAuthAuthorityKey::Process(Arc::as_ptr(store).cast::<()>() as usize)
887        })
888}
889
890fn runtime_stores_share_authority(a: &Arc<dyn RuntimeStore>, b: &Arc<dyn RuntimeStore>) -> bool {
891    match (a.auth_authority_key(), b.auth_authority_key()) {
892        (Some(a), Some(b)) => a == b,
893        _ => Arc::ptr_eq(a, b),
894    }
895}
896
897fn generated_runtime_auth_lease_handle(
898    handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
899) -> meerkat_core::handles::GeneratedAuthLeaseHandle {
900    #[allow(clippy::expect_used)]
901    crate::protocol_auth_lease_lifecycle_publication::generated_auth_lease_handle(handle)
902        .expect("runtime AuthLeaseHandle must be certified by generated AuthMachine authority")
903}
904
905#[cfg(not(target_arch = "wasm32"))]
906fn persistent_auth_authorities(
907    store: &Arc<dyn RuntimeStore>,
908) -> Arc<PersistentAuthAuthorityBundle> {
909    let key = runtime_store_identity(store);
910    let authorities = PERSISTENT_AUTH_AUTHORITIES.get_or_init(|| StdMutex::new(HashMap::new()));
911    let mut authorities = authorities
912        .lock()
913        .unwrap_or_else(std::sync::PoisonError::into_inner);
914    if let Some(existing) = authorities.get(&key) {
915        let stored_store_alive = existing
916            .store
917            .lock()
918            .unwrap_or_else(std::sync::PoisonError::into_inner)
919            .upgrade()
920            .is_some();
921        if matches!(key, PersistentAuthAuthorityKey::Durable(_)) || stored_store_alive {
922            existing.oauth_flows.bind_persistent_store(store);
923            *existing
924                .store
925                .lock()
926                .unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::downgrade(store);
927            return Arc::clone(existing);
928        }
929    }
930    let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
931    let oauth_flows = Arc::new(
932        crate::handles::RuntimeOAuthFlowHandle::new_with_persistent_store_and_auth_lease(
933            std::time::Duration::from_secs(10 * 60),
934            Arc::clone(&auth_lease),
935            store,
936        ),
937    );
938    let bundle = Arc::new(PersistentAuthAuthorityBundle {
939        store: StdMutex::new(Arc::downgrade(store)),
940        auth_lease,
941        oauth_flows,
942    });
943    authorities.insert(key, Arc::clone(&bundle));
944    bundle
945}
946
947#[cfg(all(test, not(target_arch = "wasm32")))]
948pub(crate) fn clear_persistent_auth_authorities_for_test() {
949    if let Some(authorities) = PERSISTENT_AUTH_AUTHORITIES.get() {
950        authorities
951            .lock()
952            .unwrap_or_else(std::sync::PoisonError::into_inner)
953            .clear();
954    }
955}
956
957#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
958#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
959impl BlobStore for UnavailableBlobStore {
960    async fn put_image(&self, _media_type: &str, _data: &str) -> Result<BlobRef, BlobStoreError> {
961        Err(Self::error())
962    }
963
964    async fn get(&self, _blob_id: &BlobId) -> Result<BlobPayload, BlobStoreError> {
965        Err(Self::error())
966    }
967
968    async fn delete(&self, _blob_id: &BlobId) -> Result<(), BlobStoreError> {
969        Err(Self::error())
970    }
971
972    async fn exists(&self, _blob_id: &BlobId) -> Result<bool, BlobStoreError> {
973        Err(Self::error())
974    }
975
976    fn is_persistent(&self) -> bool {
977        false
978    }
979}
980
981#[cfg(not(target_arch = "wasm32"))]
982type MeerkatMachineCommandFuture<'a> = Pin<
983    Box<
984        dyn Future<Output = Result<MeerkatMachineCommandResult, MeerkatMachineCommandError>>
985            + Send
986            + 'a,
987    >,
988>;
989
990#[cfg(target_arch = "wasm32")]
991type MeerkatMachineCommandFuture<'a> = Pin<
992    Box<dyn Future<Output = Result<MeerkatMachineCommandResult, MeerkatMachineCommandError>> + 'a>,
993>;
994
995#[cfg(test)]
996pub(crate) use driver::fail_machine_run;
997pub(crate) use driver::{
998    DriverEntry, SharedCompletionRegistry, SharedDriver, cancel_runtime_loop_run,
999    commit_machine_terminal_run, commit_runtime_loop_run, fail_runtime_loop_run,
1000    machine_authorize_runtime_loop_batch, machine_batch_primitive_projections,
1001    machine_batch_runtime_semantics, machine_commit_prepared_destroy,
1002    machine_commit_service_turn_terminal_receipt, machine_prepare_bindings_projection,
1003    machine_prepare_destroy, machine_recover_ephemeral_driver, machine_recover_persistent_driver,
1004    machine_recover_persistent_inputs, machine_recover_persistent_inputs_from_observed,
1005    machine_recycle_preserving_work, machine_reset, machine_retire, machine_stop_runtime,
1006    prepare_runtime_loop_batch_start,
1007};
1008
1009pub(crate) mod driver;
1010
1011mod comms_drain;
1012pub mod composition;
1013mod dispatch_control;
1014mod dispatch_drain;
1015mod dispatch_ingress;
1016mod dispatch_session;
1017#[allow(unused_variables, dead_code, clippy::cmp_owned)]
1018#[allow(clippy::assign_op_pattern)]
1019pub mod dsl;
1020pub(crate) mod dsl_authority;
1021mod dsl_effects;
1022mod llm_reconfigure;
1023mod runtime_control;
1024mod session_management;
1025mod traits;
1026mod visibility;
1027
1028pub(crate) use session_management::{
1029    DeleteOpsFinalizationAuthority, RetainOpsFinalizationAuthority,
1030};
1031
1032pub use composition::{MeerkatCompositionSignalDispatcher, MeerkatConsumerSurface};
1033
1034pub use comms_drain::{
1035    CommsDrainMode, CommsDrainPhase, DrainExitReason, PeerEndpointStageError, PeerIngressOwner,
1036    SupervisorBinding, SupervisorBindingStageError,
1037};
1038pub(crate) use comms_drain::{
1039    CommsDrainSlot, GeneratedSupervisorBinding, GeneratedSupervisorRotationReceipt,
1040    GeneratedSupervisorRotationSubmit, SupervisorAuthorizeAdmission, SupervisorBindAdmission,
1041    SupervisorBridgeCommandAdmission, SupervisorRotationObservation, SupervisorRotationSubmission,
1042    SupervisorRotationTaskSlot,
1043};
1044pub(crate) use dsl_effects::{DslTransitionEffects, apply_dsl_transition_on_authority};
1045pub(crate) use visibility::MachineToolVisibilityOwner;
1046
1047struct StagedSessionDslInput {
1048    previous_snapshot: dsl::MeerkatMachineAuthoritySnapshot,
1049    committed_snapshot: dsl::MeerkatMachineAuthoritySnapshot,
1050    effects: DslTransitionEffects,
1051}
1052
1053impl StagedSessionDslInput {
1054    /// True when the committed transition was a machine-owned revival of a
1055    /// stopped session (`RegisterSessionResumesStopped` /
1056    /// `EnsureSessionWithExecutorStopped`): the machine emits the typed
1057    /// `RuntimeNotice { kind: Recover }` effect, and the shell keys the
1058    /// durable lifecycle persist on it so a revived session is never left
1059    /// durably `Stopped` for cross-process readers.
1060    fn revived_stopped_session(&self) -> bool {
1061        self.effects.as_slice().iter().any(|effect| {
1062            matches!(
1063                effect,
1064                dsl::MeerkatMachineEffect::RuntimeNotice {
1065                    kind: dsl::RuntimeNoticeKind::Recover,
1066                    ..
1067                }
1068            )
1069        })
1070    }
1071
1072    /// Whether committing this already-staged transition could publish a
1073    /// cross-machine seam signal. A caller may restore `previous_snapshot`
1074    /// after dispatch failure only when this is false: once any routed signal
1075    /// may have escaped, rolling local authority back would split truth.
1076    fn has_routed_signal_effect(&self) -> bool {
1077        self.effects
1078            .as_slice()
1079            .iter()
1080            .any(|effect| composition::lift_routed_signal(effect).is_some())
1081    }
1082}
1083
1084#[derive(Clone, Copy)]
1085enum CommittedEffectDispatchFailure {
1086    PreserveCommittedDslState,
1087}
1088
1089type UnregisterTeardownResult = Result<(), RuntimeDriverError>;
1090type RuntimeStopCleanupResult = Result<(), RuntimeDriverError>;
1091
1092/// Joinable result channel for the one owned unregister saga of an epoch.
1093#[derive(Clone)]
1094struct UnregisterTeardownCoordinator {
1095    epoch_id: meerkat_core::RuntimeEpochId,
1096    coordinator_id: uuid::Uuid,
1097    result_rx: crate::tokio::sync::watch::Receiver<Option<UnregisterTeardownResult>>,
1098}
1099
1100/// Joinable result channel for the one owned ordinary-stop cleanup operation
1101/// of an epoch. Completed results remain installed until a successful resume
1102/// replaces the executor, or an explicit stop/unregister authorizes a retry.
1103#[derive(Clone)]
1104struct RuntimeStopCleanupCoordinator {
1105    epoch_id: meerkat_core::RuntimeEpochId,
1106    coordinator_id: uuid::Uuid,
1107    teardown_slot: Option<Arc<crate::runtime_loop::RuntimeLoopTeardownSlot>>,
1108    result_rx: crate::tokio::sync::watch::Receiver<Option<RuntimeStopCleanupResult>>,
1109}
1110
1111#[derive(Clone)]
1112struct PendingUnregisterFinalization {
1113    finalization_id: uuid::Uuid,
1114    durability_authority: session_management::RuntimeOpsLifecycleDurabilityAuthority,
1115    committed_snapshot: dsl::MeerkatMachineAuthoritySnapshot,
1116}
1117
1118struct UnregisterTeardownMechanicalObservations {
1119    runtime_loop_forced_abort: std::sync::atomic::AtomicBool,
1120    comms_drain_forced_abort: std::sync::atomic::AtomicBool,
1121}
1122
1123/// Owned persistence worker for one runtime's ops epoch. The unregister saga
1124/// closes the registry producer and joins this worker before the atomic store
1125/// finalization can retire the epoch.
1126#[cfg(not(target_arch = "wasm32"))]
1127struct OpsLifecyclePersistenceWorker {
1128    handle: std::thread::JoinHandle<()>,
1129}
1130
1131#[cfg(target_arch = "wasm32")]
1132struct OpsLifecyclePersistenceWorker {
1133    handle: crate::tokio::task::JoinHandle<()>,
1134}
1135
1136impl UnregisterTeardownMechanicalObservations {
1137    fn new() -> Self {
1138        Self {
1139            runtime_loop_forced_abort: std::sync::atomic::AtomicBool::new(false),
1140            comms_drain_forced_abort: std::sync::atomic::AtomicBool::new(false),
1141        }
1142    }
1143
1144    fn from_durable_process_recovery(
1145        progress: Option<&crate::store::MachineUnregisterProgressSnapshot>,
1146    ) -> Self {
1147        let observations = Self::new();
1148        if let Some(progress) = progress {
1149            // A pending producer obligation recovered in a fresh process has
1150            // lost its original JoinHandle. Closing it now is necessarily a
1151            // process-loss/forced disposition, never evidence of clean drain.
1152            observations.runtime_loop_forced_abort.store(
1153                progress.runtime_loop_drain_pending(),
1154                std::sync::atomic::Ordering::Release,
1155            );
1156            observations.comms_drain_forced_abort.store(
1157                progress.comms_drain_exit_pending(),
1158                std::sync::atomic::Ordering::Release,
1159            );
1160        }
1161        observations
1162    }
1163}
1164
1165/// Per-session state: driver + generated authority binding + shell handles.
1166struct RuntimeSessionEntry {
1167    /// Canonical runtime control-plane identity for this registered session.
1168    runtime_id: LogicalRuntimeId,
1169    /// Per-session mutation gate.
1170    ///
1171    /// Serializes same-session mutating commands across the full
1172    /// DSL-stage → driver-mutate → DSL-sync span. Without this gate,
1173    /// two concurrent commands on the same session can interleave between
1174    /// the DSL projection sync (which releases `sessions` lock) and the
1175    /// driver mutation (which acquires `driver` lock independently).
1176    ///
1177    /// This is NOT a replacement for `sessions` RwLock or `driver` Mutex —
1178    /// it is an additional serialization point that spans the entire
1179    /// multi-step mutation window.
1180    mutation_gate: Arc<Mutex<()>>,
1181    /// Serializes the complete live-open materialization window against a
1182    /// lifecycle owner's physical-absence proof + terminal marker window.
1183    /// This is separate from `mutation_gate`: live orchestration calls back
1184    /// into ordinary DSL mutations while it holds this lease.
1185    #[cfg(feature = "live")]
1186    live_lifecycle_gate: Arc<Mutex<()>>,
1187    /// Session-owned liveness driver for the currently pending supervisor
1188    /// rotation. Durable operation state remains in generated authority.
1189    supervisor_rotation_task: Arc<SupervisorRotationTaskSlot>,
1190    /// Shared driver handle (accessed by both adapter methods and RuntimeLoop).
1191    driver: SharedDriver,
1192    /// Canonical coarse control projection for this session.
1193    ///
1194    /// The driver reads this to realize shell mechanics, but machine-facing
1195    /// queries should publish from this shared cell rather than treating the
1196    /// driver shell as the source of lifecycle truth.
1197    control_projection: Arc<StdRwLock<crate::driver::ephemeral::RuntimeControlProjection>>,
1198    /// Shared async-operation lifecycle registry for this runtime/session.
1199    ops_lifecycle: Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
1200    /// Joinable durability worker for `ops_lifecycle`. Never detached: final
1201    /// unregister must prove it has observed the closed producer before the
1202    /// store records the epoch tombstone.
1203    ops_lifecycle_persistence_worker: Option<OpsLifecyclePersistenceWorker>,
1204    /// Runtime epoch identity — stable across rebuilds, rotated on reset/restart-without-recovery.
1205    epoch_id: meerkat_core::RuntimeEpochId,
1206    /// Mechanical close gate for handles minted from this session entry.
1207    ///
1208    /// The DSL still owns runtime terminality; this gate only invalidates cloned
1209    /// cross-crate handles after the entry is torn down.
1210    handle_teardown_gate: Arc<crate::handles::HandleTeardownGate>,
1211    /// Exact non-clone materialization claimant. Compatibility bindings do not
1212    /// reserve this slot; actor begin, unique preparation, attachment, and
1213    /// teardown transition it synchronously so no claimant can steal or roll
1214    /// back another transaction.
1215    materialization_claim_state:
1216        Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>,
1217    /// Shared consumer cursor state for the epoch.
1218    cursor_state: Arc<meerkat_core::EpochCursorState>,
1219    /// Completion waiters (accessed by accept_input_with_completion and RuntimeLoop).
1220    completions: SharedCompletionRegistry,
1221    /// Canonical durable visibility owner for this session.
1222    tool_visibility_owner: Arc<MachineToolVisibilityOwner>,
1223    /// One canonical epoch-local mechanical handle bundle.
1224    ///
1225    /// Actor reconstruction for an unchanged attachment clones these exact
1226    /// Arcs and replaces only its one-shot materialization authority. Minting
1227    /// fresh surface handles for the same epoch would replay MCP lifecycle
1228    /// transitions and split handle identity from the attached executor.
1229    canonical_runtime_bindings: Option<meerkat_core::SessionRuntimeBindings>,
1230    /// Runtime-loop channel publication slot.
1231    ///
1232    /// This is mechanical shell state only. The generated `MeerkatMachine`
1233    /// `registration_phase` is the semantic executor registration authority.
1234    attachment_slot: RuntimeLoopAttachmentSlot,
1235    /// Exact executor-cleanup handoff retained across unregister retries.
1236    ///
1237    /// The attachment channels and loop JoinHandle are consumed by the first
1238    /// teardown attempt, but a failing external cleanup restores its executor
1239    /// into this slot so the next machine-owned saga can retry without
1240    /// fabricating quiescence.
1241    runtime_loop_teardown: Option<Arc<crate::runtime_loop::RuntimeLoopTeardownSlot>>,
1242    /// Current epoch's owned unregister saga, if one is active.
1243    unregister_coordinator: Option<UnregisterTeardownCoordinator>,
1244    /// Current epoch's single ordinary-stop cleanup owner. Unlike unregister,
1245    /// its successful terminal leaves this registration in generated Stopped.
1246    runtime_stop_cleanup_coordinator: Option<RuntimeStopCleanupCoordinator>,
1247    /// A missing-live materialization normalized stale executor authority but
1248    /// has not yet committed its replacement attachment. The next exact
1249    /// attachment commit owns the matching durable lifecycle publication.
1250    pending_revival_lifecycle_persist: Arc<std::sync::atomic::AtomicBool>,
1251    /// Retry witness installed before the final generated UnregisterSession
1252    /// transition. If an owned saga panics after that transition changes the
1253    /// live projection to Queuing/session_id=None, the next saga resumes the
1254    /// same atomic store commit instead of trying to BeginUnregister again.
1255    pending_unregister_finalization: Option<PendingUnregisterFinalization>,
1256    /// Cancellation-safe shell observations waiting to be committed through
1257    /// the matching generated unregister feedback inputs.
1258    unregister_teardown_observations: Arc<UnregisterTeardownMechanicalObservations>,
1259    /// Durable-session publication capability retained independently of the
1260    /// loop channels. Unregister detaches those channels before its final
1261    /// terminal sweep; publication retries must still use the same owning
1262    /// session surface rather than dropping the outbox or inventing another
1263    /// publisher.
1264    publication_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorPublicationHandle>>,
1265    /// Cloneable surface/service teardown retained across loop detachment so
1266    /// external unregister and retry-finalize paths can finish the same exact
1267    /// cleanup transaction.
1268    post_stop_cleanup_handle:
1269        Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorPostStopCleanupHandle>>,
1270    /// Attachment incarnation authorized to run `post_stop_cleanup_handle`.
1271    post_stop_cleanup_attachment_id: Option<RuntimeLoopAttachmentId>,
1272    /// Whether the retained cleanup completed for that incarnation.
1273    post_stop_cleanup_complete: bool,
1274    /// Serializes cloneable cleanup attempts for the current attachment while
1275    /// the machine mutation gate is deliberately released. External
1276    /// unregister, loop-owned unregister, and final-unregister retry may race
1277    /// in `Draining`; exactly one of them may call the surface cleanup handle
1278    /// at a time.
1279    post_stop_cleanup_gate: Arc<Mutex<()>>,
1280    /// Temporary live interrupt capability for prepared, session-owned turns
1281    /// that run before the runtime loop attachment is published.
1282    provisional_interrupt_handle:
1283        Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
1284    /// Exact prepared-materialization transaction that installed the
1285    /// provisional interrupt/cleanup pair. A stale binding cannot replace or
1286    /// clear another transaction's provisional ownership.
1287    provisional_materialization_claim_id: Option<uuid::Uuid>,
1288    /// DSL authority for coarse lifecycle phase transitions.
1289    /// Sync field — validates transitions, writes back phase.
1290    ///
1291    /// `Arc<std::sync::Mutex<_>>` so cross-crate handle impls
1292    /// (`meerkat-runtime::handles::*`) can share the same underlying authority
1293    /// from a sync context without awaiting the outer `sessions` tokio lock.
1294    /// The Arc heap-allocates the authority's large expanded state (31 fields
1295    /// including several Maps/Sets) so holding a reference to a
1296    /// `RuntimeSessionEntry` does not bloat async future sizes.
1297    dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1298    /// Per-session comms drain lifecycle slot.
1299    ///
1300    /// Collapsed from the sibling `MeerkatMachine.comms_drain_slots:
1301    /// RwLock<HashMap<SessionId, CommsDrainSlot>>` in wave-c C-H2 (F5 in
1302    /// docs/wave-c-prep/state-scope-audit.md) — keeping the slot here
1303    /// makes "session exists" a single HashMap insertion and eliminates
1304    /// the class of bugs where the sibling map and the session map
1305    /// could fall out of sync across a registration/unregistration
1306    /// boundary.
1307    drain_slot: CommsDrainSlot,
1308}
1309
1310#[derive(Clone)]
1311struct MemberIncarnationRegistration {
1312    incarnation: meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
1313    session_mutation_gate: Arc<Mutex<()>>,
1314    tracked_turn_journal: Option<Arc<dyn crate::member_observation::TrackedTurnJournal>>,
1315}
1316
1317enum MemberResidencyState {
1318    /// Ordinary runtime session with no host-placement authority.
1319    PeerOnly,
1320    /// A host-owned session id whose placed incarnation is absent, released,
1321    /// or in cutover. This must never be treated as peer-only.
1322    VacantPlaced,
1323    /// Exact current host placement plus its runtime/journal authorities.
1324    Placed(MemberIncarnationRegistration),
1325}
1326
1327struct MemberResidencySlot {
1328    gate: Arc<Mutex<()>>,
1329    state: StdRwLock<MemberResidencyState>,
1330}
1331
1332struct MemberEffectAuthorityLease {
1333    slot: Arc<MemberResidencySlot>,
1334    slot_guard: crate::tokio::sync::OwnedMutexGuard<()>,
1335    session_mutation_gate: Arc<Mutex<()>>,
1336}
1337
1338/// Exact effect interval for a host-member residency. The stable residency
1339/// slot blocks G1→G2/vacate publication while the captured session guard
1340/// blocks unregister/same-SessionId runtime replacement. Neither authority
1341/// may be dropped before the caller's effect completes.
1342pub(crate) struct MemberEffectAuthorityGuard {
1343    _slot_guard: crate::tokio::sync::OwnedMutexGuard<()>,
1344    _session_guard: crate::tokio::sync::OwnedMutexGuard<()>,
1345}
1346
1347impl MemberResidencySlot {
1348    fn peer_only() -> Self {
1349        Self {
1350            gate: Arc::new(Mutex::new(())),
1351            state: StdRwLock::new(MemberResidencyState::PeerOnly),
1352        }
1353    }
1354}
1355
1356/// Non-cloneable lease that holds one session's mutation authority across a
1357/// cross-crate archive transaction. The session service acquires this before
1358/// its recovery/checkpointer gates, then realizes Retire through the captured
1359/// driver without re-locking the mutation gate.
1360pub struct MachineSessionArchiveLease {
1361    session_id: SessionId,
1362    runtime_id: LogicalRuntimeId,
1363    driver: SharedDriver,
1364    completions: SharedCompletionRegistry,
1365    wake_tx: Option<mpsc::Sender<()>>,
1366    publication_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorPublicationHandle>>,
1367    /// True only when archive recovery itself inserted the in-memory runtime
1368    /// registration from durable authority. A quiescent terminal lease with
1369    /// this witness may remove that reconstructable registration without
1370    /// touching durable lifecycle truth; a registration that predated archive
1371    /// must never be removed through that cleanup path.
1372    recovered_registration_for_archive: bool,
1373    /// Stable absent-entry/register/unregister transaction boundary. It is
1374    /// acquired before the live and mutation leases and retained through any
1375    /// archive-only removal of the recovered entry.
1376    _registration_transaction_guard: crate::tokio::sync::OwnedMutexGuard<()>,
1377    /// Shared live/lifecycle boundary retained from physical absence proof
1378    /// through the durable archive/retire marker.
1379    _live_lifecycle_lease: Option<crate::member_live::MemberLiveLifecycleLease>,
1380    _mutation_guard: crate::tokio::sync::OwnedMutexGuard<()>,
1381}
1382
1383/// Selects whether local materialization preserves ordinary authority or
1384/// performs the narrow machine-owned missing-live normalization first.
1385#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1386pub enum LocalSessionMaterializationMode {
1387    /// Prepare local session resources without replacing a machine-authorized
1388    /// executor binding.
1389    #[default]
1390    Ordinary,
1391    /// Normalize the narrow ownerless executor shape authorized by Mob
1392    /// missing-live revival before preparing its replacement attachment.
1393    MissingLiveRevival,
1394}
1395
1396/// Unique, cancellation-safe owner of one prepared session materialization.
1397///
1398/// Runtime bindings remain cloneable factory data; this lease is deliberately
1399/// non-clone so rollback ownership can only move between orchestration layers by
1400/// an explicit Rust move. Dropping an armed lease synchronously fences executor
1401/// attachment, then schedules exact provisional cleanup/registration rollback.
1402pub struct PreparedSessionMaterialization {
1403    machine: Arc<MeerkatMachine>,
1404    bindings: meerkat_core::SessionRuntimeBindings,
1405    claim_id: uuid::Uuid,
1406    claim_state: Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>,
1407    cleanup_spawner: MachineCleanupTaskSpawner,
1408    archived_resume_authorization_issued: bool,
1409    armed: bool,
1410}
1411
1412/// Exact post-actor commit authority for an Archived+Retired materialization.
1413///
1414/// This non-clone lease retains the machine mutation gate while the session
1415/// owner promotes its durable document and while the runtime performs the
1416/// matching Retired -> Idle transition. A broad session-control token cannot
1417/// cross either boundary while an exact materialization claim is outstanding.
1418pub struct PreparedArchivedResumeCommitLease {
1419    machine: Arc<MeerkatMachine>,
1420    session_id: SessionId,
1421    epoch_id: meerkat_core::RuntimeEpochId,
1422    claim_id: uuid::Uuid,
1423    claim_state: Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>,
1424    _mutation_guard: crate::tokio::sync::OwnedMutexGuard<()>,
1425}
1426
1427/// Type-state proof that the session owner durably promoted the archived
1428/// document through the same runtime store authority as this exact machine
1429/// attachment claim.
1430///
1431/// Only this promoted form can realize Retired -> Idle, making the required
1432/// document-before-runtime ordering explicit at the API boundary.
1433pub struct PromotedArchivedResumeCommitLease {
1434    prepared: PreparedArchivedResumeCommitLease,
1435}
1436
1437impl PreparedArchivedResumeCommitLease {
1438    pub fn session_id(&self) -> &SessionId {
1439        &self.session_id
1440    }
1441
1442    /// Convert the exact prepared lease into its post-document-promotion
1443    /// type state. The session service calls this only after its durable
1444    /// document write succeeds, and the shared-store check prevents a foreign
1445    /// service from authorizing this machine's runtime reset.
1446    pub fn confirm_document_promoted(
1447        self,
1448        runtime_store: &Arc<dyn crate::RuntimeStore>,
1449    ) -> Result<PromotedArchivedResumeCommitLease, RuntimeDriverError> {
1450        if !self.machine.shares_runtime_store_authority(runtime_store) {
1451            return Err(RuntimeDriverError::StaleAuthority {
1452                reason: format!(
1453                    "archived document promotion for session {} used a runtime store not owned by the prepared machine",
1454                    self.session_id
1455                ),
1456            });
1457        }
1458        Ok(PromotedArchivedResumeCommitLease { prepared: self })
1459    }
1460}
1461
1462impl PromotedArchivedResumeCommitLease {
1463    pub fn session_id(&self) -> &SessionId {
1464        self.prepared.session_id()
1465    }
1466
1467    /// Realize the exact Retired -> Idle half after durable document
1468    /// promotion has been certified by the session owner.
1469    pub async fn reset_retired_runtime(&mut self) -> Result<ResetReport, RuntimeDriverError> {
1470        self.prepared
1471            .machine
1472            .reset_runtime_for_promoted_archived_resume(self)
1473            .await
1474    }
1475}
1476
1477/// One-shot authorization to construct an actor at the Archived+Retired
1478/// revival midpoint for one exact prepared machine registration.
1479///
1480/// This is deliberately non-cloneable and non-constructible outside the
1481/// machine. It carries the exact claim and registration identities, while
1482/// [`begin_session_runtime_actor_materialization_for_archived_resume`](crate::begin_session_runtime_actor_materialization_for_archived_resume)
1483/// acquires the current machine mutation gate and revalidates those identities
1484/// plus the exact `Retired` lifecycle phase before actor construction starts.
1485#[must_use = "archived-resume authorization must be consumed by actor materialization"]
1486pub struct ArchivedSessionActorMaterializationAuthorization {
1487    machine: Arc<MeerkatMachine>,
1488    session_id: SessionId,
1489    epoch_id: meerkat_core::RuntimeEpochId,
1490    claim_id: uuid::Uuid,
1491    claim_state: Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>,
1492    dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
1493    teardown_gate: Arc<crate::handles::HandleTeardownGate>,
1494}
1495
1496impl ArchivedSessionActorMaterializationAuthorization {
1497    pub(crate) async fn begin(
1498        self,
1499        bindings: &meerkat_core::SessionRuntimeBindings,
1500    ) -> Result<crate::RuntimeActorMaterializationPermit, crate::RuntimeActorMaterializationError>
1501    {
1502        let binding_authority = crate::validated_session_runtime_bindings_authority(bindings)?;
1503        if bindings.session_id() != &self.session_id
1504            || bindings.epoch_id() != &self.epoch_id
1505            || binding_authority.materialization_claim_id != Some(self.claim_id)
1506            || !Arc::ptr_eq(
1507                &binding_authority.materialization_claim_state,
1508                &self.claim_state,
1509            )
1510            || !Arc::ptr_eq(&binding_authority.dsl_authority, &self.dsl_authority)
1511            || !Arc::ptr_eq(&binding_authority.teardown_gate, &self.teardown_gate)
1512        {
1513            return Err(crate::RuntimeActorMaterializationError::InvalidAuthority(
1514                "archived-resume authorization does not match the exact prepared binding"
1515                    .to_string(),
1516            ));
1517        }
1518
1519        let mutation_guard = self
1520            .machine
1521            .lock_current_session_mutation_gate(&self.session_id)
1522            .await
1523            .ok_or(crate::RuntimeActorMaterializationError::RegistrationClosed)?;
1524        {
1525            let sessions = self.machine.sessions.read().await;
1526            let entry = sessions
1527                .get(&self.session_id)
1528                .ok_or(crate::RuntimeActorMaterializationError::RegistrationClosed)?;
1529            let exact_registration = entry.epoch_id == self.epoch_id
1530                && Arc::ptr_eq(&entry.materialization_claim_state, &self.claim_state)
1531                && Arc::ptr_eq(&entry.dsl_authority, &self.dsl_authority)
1532                && Arc::ptr_eq(&entry.handle_teardown_gate, &self.teardown_gate)
1533                && entry.provisional_materialization_claim_id == Some(self.claim_id)
1534                && !entry.physical_attachment_is_live();
1535            let exact_claim = self
1536                .claim_state
1537                .lock()
1538                .unwrap_or_else(std::sync::PoisonError::into_inner)
1539                .exact_claim_is(
1540                    self.claim_id,
1541                    &[
1542                        crate::RuntimeActorMaterializationClaimPhase::Prepared,
1543                        crate::RuntimeActorMaterializationClaimPhase::Staged,
1544                    ],
1545                );
1546            if !exact_registration || !exact_claim {
1547                return Err(crate::RuntimeActorMaterializationError::RegistrationClosed);
1548            }
1549        }
1550
1551        crate::begin_session_runtime_actor_materialization_with_phase_policy(
1552            bindings,
1553            crate::RuntimeActorMaterializationPhasePolicy::RequireArchivedRevivalBoundary,
1554            Some(mutation_guard),
1555        )
1556    }
1557}
1558
1559/// Exact actor-only recovery lease for a session whose executor attachment is
1560/// already committed and serving.
1561///
1562/// The lease holds the machine mutation gate for that attachment, so service
1563/// actor reconstruction cannot race executor replacement or unregister. It
1564/// owns no registration/attachment rollback authority and can never spawn a
1565/// second runtime loop.
1566pub struct PreparedAttachedSessionActorRecovery {
1567    bindings: meerkat_core::SessionRuntimeBindings,
1568    witness: RuntimeExecutorAttachmentWitness,
1569    claim_id: uuid::Uuid,
1570    claim_state: Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>,
1571    previous_phase: crate::RuntimeActorMaterializationClaimPhase,
1572    mutation_guard: Option<crate::tokio::sync::OwnedMutexGuard<()>>,
1573    armed: bool,
1574}
1575
1576/// Exact actor-materialization authority that may be consumed by one executor
1577/// attachment attempt. This remains crate-private: surfaces transfer the
1578/// non-clone [`PreparedSessionMaterialization`] lease rather than minting or
1579/// comparing claim identifiers themselves.
1580#[derive(Clone)]
1581struct RuntimeExecutorAttachmentMaterializationClaim {
1582    claim_id: uuid::Uuid,
1583    claim_state: Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>,
1584    epoch_id: meerkat_core::RuntimeEpochId,
1585}
1586
1587/// Opaque identity for one exact runtime-session registration.
1588///
1589/// Unlike [`RuntimeExecutorAttachmentWitness`], this witness deliberately
1590/// carries no executor identity. It exists for machine-owned cleanup of a
1591/// terminal registration that never published an attachment (for example, a
1592/// registration materialized only to recover its durable ops lifecycle).
1593/// Callers may clone and compare the witness, but only this machine can use it
1594/// to admit exact compare-and-remove teardown. Durable epoch identity alone is
1595/// not exact because an epoch may survive an in-process entry rebuild; the
1596/// private weak mutation-gate identity distinguishes those incarnations.
1597#[derive(Clone)]
1598pub struct RuntimeSessionRegistrationWitness {
1599    machine: std::sync::Weak<MeerkatMachineShared>,
1600    session_id: SessionId,
1601    epoch_id: meerkat_core::RuntimeEpochId,
1602    registration_gate: std::sync::Weak<Mutex<()>>,
1603}
1604
1605impl RuntimeSessionRegistrationWitness {
1606    fn new(
1607        machine: std::sync::Weak<MeerkatMachineShared>,
1608        session_id: SessionId,
1609        epoch_id: meerkat_core::RuntimeEpochId,
1610        registration_gate: std::sync::Weak<Mutex<()>>,
1611    ) -> Self {
1612        Self {
1613            machine,
1614            session_id,
1615            epoch_id,
1616            registration_gate,
1617        }
1618    }
1619
1620    pub fn session_id(&self) -> &SessionId {
1621        &self.session_id
1622    }
1623
1624    pub fn epoch_id(&self) -> &meerkat_core::RuntimeEpochId {
1625        &self.epoch_id
1626    }
1627
1628    fn belongs_to(&self, machine: &MeerkatMachine) -> bool {
1629        std::ptr::eq(self.machine.as_ptr(), Arc::as_ptr(&machine.shared))
1630    }
1631
1632    fn matches_entry(&self, entry: &RuntimeSessionEntry) -> bool {
1633        self.registration_gate
1634            .upgrade()
1635            .is_some_and(|gate| Arc::ptr_eq(&gate, &entry.mutation_gate))
1636    }
1637
1638    fn registration_gate(&self) -> Option<Arc<Mutex<()>>> {
1639        self.registration_gate.upgrade()
1640    }
1641}
1642
1643impl std::fmt::Debug for RuntimeSessionRegistrationWitness {
1644    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1645        formatter
1646            .debug_struct("RuntimeSessionRegistrationWitness")
1647            .field("session_id", &self.session_id)
1648            .field("epoch_id", &self.epoch_id)
1649            .finish_non_exhaustive()
1650    }
1651}
1652
1653impl PartialEq for RuntimeSessionRegistrationWitness {
1654    fn eq(&self, other: &Self) -> bool {
1655        self.session_id == other.session_id
1656            && self.epoch_id == other.epoch_id
1657            && std::sync::Weak::ptr_eq(&self.machine, &other.machine)
1658            && std::sync::Weak::ptr_eq(&self.registration_gate, &other.registration_gate)
1659    }
1660}
1661
1662impl Eq for RuntimeSessionRegistrationWitness {}
1663
1664/// Exact durable lifecycle observation for one session registration target.
1665///
1666/// Construction is owned by MeerkatMachine so a raw-content version observed
1667/// for one runtime id cannot be replayed as the precondition for another.
1668#[derive(Debug, Clone, PartialEq, Eq)]
1669pub struct RuntimeSessionLifecycleObservation {
1670    session_id: SessionId,
1671    runtime_id: LogicalRuntimeId,
1672    lifecycle: crate::store::MachineLifecycleObservation,
1673}
1674
1675impl RuntimeSessionLifecycleObservation {
1676    fn new(
1677        session_id: SessionId,
1678        runtime_id: LogicalRuntimeId,
1679        lifecycle: crate::store::MachineLifecycleObservation,
1680    ) -> Self {
1681        Self {
1682            session_id,
1683            runtime_id,
1684            lifecycle,
1685        }
1686    }
1687
1688    #[must_use]
1689    pub fn session_id(&self) -> &SessionId {
1690        &self.session_id
1691    }
1692
1693    #[must_use]
1694    pub fn runtime_id(&self) -> &LogicalRuntimeId {
1695        &self.runtime_id
1696    }
1697
1698    #[must_use]
1699    pub fn lifecycle(&self) -> &crate::store::MachineLifecycleObservation {
1700        &self.lifecycle
1701    }
1702}
1703
1704/// Result of one observation-bound, externally fenced cold registration.
1705///
1706/// The durable lifecycle observation is target-local runtime content. The
1707/// external write fence is evaluated separately and is never encoded into that
1708/// content or its version.
1709#[derive(Debug, Clone, PartialEq, Eq)]
1710pub enum RuntimeSessionRegistrationOutcome {
1711    /// The exact observed lifecycle row was replaced with the canonical fresh
1712    /// Idle shell and the matching live registration was published.
1713    Applied {
1714        registration: RuntimeSessionRegistrationWitness,
1715        lifecycle: RuntimeSessionLifecycleObservation,
1716    },
1717    /// The exact durable row was already the canonical fresh Idle shell. Its
1718    /// external fence was still checked and the matching live registration was
1719    /// published.
1720    AlreadyExact {
1721        registration: RuntimeSessionRegistrationWitness,
1722        lifecycle: RuntimeSessionLifecycleObservation,
1723    },
1724    /// Either the target observation or the external write authority changed.
1725    /// No live registration was published by this invocation.
1726    Conflict {
1727        current: RuntimeSessionLifecycleObservation,
1728        reason: String,
1729    },
1730    /// The exact row or required store capability cannot be repaired safely.
1731    RepairBlocked {
1732        evidence_digest: Option<String>,
1733        reason: String,
1734    },
1735    /// Observation, fence admission, or target persistence was temporarily
1736    /// unavailable. The caller should re-observe and retry.
1737    Backoff { reason: String },
1738}
1739
1740/// Opaque identity for one exact runtime-executor attachment.
1741///
1742/// The machine mints this witness before constructing the executor so
1743/// surface-owned cleanup handles can retain the same identity.  It is
1744/// deliberately non-serializable and exposes no raw attachment identifier;
1745/// callers may clone and compare it, but cannot manufacture authority over a
1746/// replacement attachment.
1747#[derive(Clone)]
1748pub struct RuntimeExecutorAttachmentWitness {
1749    machine: std::sync::Weak<MeerkatMachineShared>,
1750    session_id: SessionId,
1751    epoch_id: meerkat_core::RuntimeEpochId,
1752    attachment_id: RuntimeLoopAttachmentId,
1753}
1754
1755impl RuntimeExecutorAttachmentWitness {
1756    fn new(
1757        machine: std::sync::Weak<MeerkatMachineShared>,
1758        session_id: SessionId,
1759        epoch_id: meerkat_core::RuntimeEpochId,
1760        attachment_id: RuntimeLoopAttachmentId,
1761    ) -> Self {
1762        Self {
1763            machine,
1764            session_id,
1765            epoch_id,
1766            attachment_id,
1767        }
1768    }
1769
1770    pub fn session_id(&self) -> &SessionId {
1771        &self.session_id
1772    }
1773
1774    pub fn epoch_id(&self) -> &meerkat_core::RuntimeEpochId {
1775        &self.epoch_id
1776    }
1777
1778    fn belongs_to(&self, machine: &MeerkatMachine) -> bool {
1779        std::ptr::eq(self.machine.as_ptr(), Arc::as_ptr(&machine.shared))
1780    }
1781}
1782
1783impl std::fmt::Debug for RuntimeExecutorAttachmentWitness {
1784    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1785        formatter
1786            .debug_struct("RuntimeExecutorAttachmentWitness")
1787            .field("session_id", &self.session_id)
1788            .field("epoch_id", &self.epoch_id)
1789            .finish_non_exhaustive()
1790    }
1791}
1792
1793impl PartialEq for RuntimeExecutorAttachmentWitness {
1794    fn eq(&self, other: &Self) -> bool {
1795        self.session_id == other.session_id
1796            && self.epoch_id == other.epoch_id
1797            && self.attachment_id == other.attachment_id
1798            && std::sync::Weak::ptr_eq(&self.machine, &other.machine)
1799    }
1800}
1801
1802impl Eq for RuntimeExecutorAttachmentWitness {}
1803
1804/// Exact result of ensuring a runtime executor.
1805///
1806/// `Existing` proves a committed, serving attachment already owns the
1807/// session. `Pending` owns a newly attached/startup-reconciled executor whose
1808/// serving capabilities remain unpublished until the surface commits its
1809/// exact sidecars.
1810pub enum EnsureRuntimeExecutorAttachment {
1811    Existing(RuntimeExecutorAttachmentWitness),
1812    Pending(PendingRuntimeExecutorAttachment),
1813}
1814
1815/// Process-lifetime executor for machine cleanup work transferred out of an
1816/// RAII lease.
1817///
1818/// Native Tokio handles do allow cross-thread spawning, but once their owning
1819/// application runtime is dropped a newly spawned task is immediately
1820/// cancelled. Exact attachment cleanup must outlive both the task that opened
1821/// the lease and that task's runtime, so native builds use one runtime owned by
1822/// this crate for the rest of the process. WebAssembly already has a
1823/// process-wide JavaScript event loop and `tokio_with_wasm::spawn` is not tied
1824/// to an application-owned Tokio runtime. The slot caches only a successfully
1825/// built runtime: resource exhaustion during the first attempt is retryable.
1826#[cfg(not(target_arch = "wasm32"))]
1827static MACHINE_CLEANUP_RUNTIME: OnceLock<StdMutex<Option<crate::tokio::runtime::Runtime>>> =
1828    OnceLock::new();
1829
1830/// Cloneable proof that cancellation-safe machine cleanup can be dispatched.
1831///
1832/// Every RAII owner with asynchronous cleanup acquires and stores this before
1833/// it receives cleanup authority. Its `spawn` operation is therefore
1834/// infallible with respect to runtime initialization and never depends on the
1835/// caller's ambient runtime.
1836#[derive(Clone)]
1837struct MachineCleanupTaskSpawner {
1838    #[cfg(not(target_arch = "wasm32"))]
1839    handle: crate::tokio::runtime::Handle,
1840}
1841
1842/// Process-lifetime cleanup dispatcher for surface transactions that must
1843/// outlive the ambient Tokio runtime which opened them.
1844#[doc(hidden)]
1845#[derive(Clone)]
1846pub struct RuntimeCleanupTaskSpawner {
1847    inner: MachineCleanupTaskSpawner,
1848}
1849
1850impl RuntimeCleanupTaskSpawner {
1851    pub fn acquire() -> Result<Self, RuntimeDriverError> {
1852        Ok(Self {
1853            inner: MachineCleanupTaskSpawner::acquire()?,
1854        })
1855    }
1856
1857    #[cfg(not(target_arch = "wasm32"))]
1858    pub fn spawn_detached<F>(&self, future: F)
1859    where
1860        F: Future<Output = ()> + Send + 'static,
1861    {
1862        self.inner.spawn(future);
1863    }
1864
1865    #[cfg(target_arch = "wasm32")]
1866    pub fn spawn_detached<F>(&self, future: F)
1867    where
1868        F: Future<Output = ()> + 'static,
1869    {
1870        self.inner.spawn(future);
1871    }
1872}
1873
1874impl MachineCleanupTaskSpawner {
1875    #[cfg(not(target_arch = "wasm32"))]
1876    fn acquire() -> Result<Self, RuntimeDriverError> {
1877        let slot = MACHINE_CLEANUP_RUNTIME.get_or_init(|| StdMutex::new(None));
1878        let mut runtime = slot
1879            .lock()
1880            .unwrap_or_else(std::sync::PoisonError::into_inner);
1881        if let Some(runtime) = runtime.as_ref() {
1882            return Ok(Self {
1883                handle: runtime.handle().clone(),
1884            });
1885        }
1886
1887        let candidate = crate::tokio::runtime::Builder::new_multi_thread()
1888            .worker_threads(1)
1889            .thread_name("meerkat-machine-cleanup")
1890            .enable_all()
1891            .build()
1892            .map_err(|error| {
1893                RuntimeDriverError::Internal(format!(
1894                    "failed to start machine cleanup runtime: {error}"
1895                ))
1896            })?;
1897        let handle = candidate.handle().clone();
1898        *runtime = Some(candidate);
1899        Ok(Self { handle })
1900    }
1901
1902    #[cfg(target_arch = "wasm32")]
1903    fn acquire() -> Result<Self, RuntimeDriverError> {
1904        Ok(Self {})
1905    }
1906
1907    #[cfg(not(target_arch = "wasm32"))]
1908    fn spawn<F, T>(&self, future: F) -> crate::tokio::task::JoinHandle<T>
1909    where
1910        F: Future<Output = T> + Send + 'static,
1911        T: Send + 'static,
1912    {
1913        self.handle.spawn(future)
1914    }
1915
1916    #[cfg(target_arch = "wasm32")]
1917    fn spawn<F, T>(&self, future: F) -> crate::tokio::task::JoinHandle<T>
1918    where
1919        F: Future<Output = T> + 'static,
1920        T: 'static,
1921    {
1922        crate::tokio::spawn(future)
1923    }
1924}
1925
1926impl std::fmt::Debug for EnsureRuntimeExecutorAttachment {
1927    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1928        match self {
1929            Self::Existing(witness) => formatter.debug_tuple("Existing").field(witness).finish(),
1930            Self::Pending(pending) => formatter.debug_tuple("Pending").field(pending).finish(),
1931        }
1932    }
1933}
1934
1935/// Non-cloneable commit lease for one exact newly attached executor.
1936///
1937/// The runtime loop has completed startup reconciliation, but the machine
1938/// withholds serving publication and wake effects while this lease is armed.
1939/// A surface may publish sidecars tagged with [`Self::witness`] and then call
1940/// [`Self::commit`]. Dropping the lease transfers its mutation fence into an
1941/// independently owned exact-unregister saga, so caller cancellation cannot
1942/// strand a live executor or tear down a same-SessionId replacement.
1943#[must_use = "dropping a pending runtime attachment aborts that exact attachment"]
1944pub struct PendingRuntimeExecutorAttachment {
1945    machine: Arc<MeerkatMachine>,
1946    witness: RuntimeExecutorAttachmentWitness,
1947    mutation_guard: Option<crate::tokio::sync::OwnedMutexGuard<()>>,
1948    cleanup_spawner: MachineCleanupTaskSpawner,
1949    should_wake: bool,
1950    persist_lifecycle_on_commit: bool,
1951    armed: bool,
1952}
1953
1954/// Exact post-startup publication fence retained by a surface until its own
1955/// durable ownership row is committed.
1956///
1957/// The runtime loop remains mechanically pending and non-serving while the
1958/// same M guard is held. The surface must either activate its sidecar through
1959/// [`Self::commit_with`] or retire the exact attachment. Drop transfers
1960/// retirement to the owned cleanup runtime, so cancellation cannot publish a
1961/// dead host residency.
1962#[must_use = "dropping a retained runtime attachment aborts that exact attachment"]
1963pub struct CommittedRuntimeExecutorAttachmentPublicationLease {
1964    machine: Arc<MeerkatMachine>,
1965    witness: RuntimeExecutorAttachmentWitness,
1966    mutation_guard: Option<crate::tokio::sync::OwnedMutexGuard<()>>,
1967    cleanup_spawner: MachineCleanupTaskSpawner,
1968    should_wake: bool,
1969    persist_lifecycle_on_commit: bool,
1970    armed: bool,
1971}
1972
1973/// Exact, cancellation-safe retirement intent for one committed executor
1974/// attachment.
1975///
1976/// The caller must acquire the service turn-finalization boundary before the
1977/// machine creates this lease. While armed, the retained machine mutation gate
1978/// prevents same-SessionId replacement before canonical unregister reaches
1979/// its attachment-local post-stop actor cleanup.
1980/// Dropping the lease starts the same exact retirement saga as [`Self::commit`]
1981/// so cancellation cannot strand the serving executor or its actor.
1982#[must_use = "dropping a prepared runtime attachment retirement starts exact retirement"]
1983pub struct PreparedRuntimeExecutorAttachmentRetirement {
1984    machine: Arc<MeerkatMachine>,
1985    witness: RuntimeExecutorAttachmentWitness,
1986    mutation_guard: Option<crate::tokio::sync::OwnedMutexGuard<()>>,
1987    cleanup_spawner: MachineCleanupTaskSpawner,
1988    armed: bool,
1989}
1990
1991/// Completion handle for an independently owned exact-attachment retirement.
1992///
1993/// Dropping this handle never cancels retirement. Callers that hold the
1994/// service turn-finalization boundary must release it before awaiting because
1995/// machine unregister reacquires that boundary for post-stop cleanup.
1996#[must_use = "drop is cancellation-safe, but await completion to observe retirement errors"]
1997pub struct RuntimeExecutorAttachmentRetirementCompletion {
1998    result_rx: crate::tokio::sync::oneshot::Receiver<Result<bool, RuntimeDriverError>>,
1999}
2000
2001impl RuntimeExecutorAttachmentRetirementCompletion {
2002    fn new(
2003        result_rx: crate::tokio::sync::oneshot::Receiver<Result<bool, RuntimeDriverError>>,
2004    ) -> Self {
2005        Self { result_rx }
2006    }
2007
2008    pub async fn wait(self) -> Result<bool, RuntimeDriverError> {
2009        self.result_rx.await.map_err(|error| {
2010            RuntimeDriverError::Internal(format!(
2011                "owned exact runtime attachment retirement ended without a result: {error}"
2012            ))
2013        })?
2014    }
2015}
2016
2017impl PreparedRuntimeExecutorAttachmentRetirement {
2018    fn new(
2019        machine: Arc<MeerkatMachine>,
2020        witness: RuntimeExecutorAttachmentWitness,
2021        mutation_guard: crate::tokio::sync::OwnedMutexGuard<()>,
2022        cleanup_spawner: MachineCleanupTaskSpawner,
2023    ) -> Self {
2024        Self {
2025            machine,
2026            witness,
2027            mutation_guard: Some(mutation_guard),
2028            cleanup_spawner,
2029            armed: true,
2030        }
2031    }
2032
2033    pub fn witness(&self) -> &RuntimeExecutorAttachmentWitness {
2034        &self.witness
2035    }
2036
2037    /// Transfer the retained mutation fence into the independently owned
2038    /// exact-unregister saga. This method is synchronous so the shared surface
2039    /// can release its service boundary before awaiting completion.
2040    pub fn commit(
2041        mut self,
2042    ) -> Result<RuntimeExecutorAttachmentRetirementCompletion, RuntimeDriverError> {
2043        let guard = self.mutation_guard.take().ok_or_else(|| {
2044            RuntimeDriverError::Internal(
2045                "prepared exact attachment retirement lost its mutation fence".to_string(),
2046            )
2047        })?;
2048        let completion = self
2049            .machine
2050            .spawn_executor_attachment_retirement_with_guard(
2051                self.witness.clone(),
2052                guard,
2053                self.cleanup_spawner.clone(),
2054            );
2055        self.armed = false;
2056        Ok(completion)
2057    }
2058
2059    /// Complete exact attachment retirement while the caller retains the
2060    /// service turn-finalization boundary that preceded this lease's M guard.
2061    /// The attachment-local actor cleanup therefore runs in B -> M -> R order;
2062    /// canonical unregister observes it complete and cannot re-enter B.
2063    pub async fn commit_under_runtime_turn_finalization_boundary(
2064        mut self,
2065    ) -> Result<bool, RuntimeDriverError> {
2066        self.machine
2067            .complete_executor_attachment_cleanup_under_runtime_turn_boundary(&self.witness)
2068            .await?;
2069        let guard = self.mutation_guard.take().ok_or_else(|| {
2070            RuntimeDriverError::Internal(
2071                "prepared exact attachment retirement lost its mutation fence".to_string(),
2072            )
2073        })?;
2074        self.armed = false;
2075        self.machine
2076            .unregister_executor_attachment_if_current_with_guard(self.witness.clone(), guard)
2077            .await
2078    }
2079}
2080
2081impl Drop for PreparedRuntimeExecutorAttachmentRetirement {
2082    fn drop(&mut self) {
2083        if !self.armed {
2084            return;
2085        }
2086        let Some(guard) = self.mutation_guard.take() else {
2087            return;
2088        };
2089        let _completion = self
2090            .machine
2091            .spawn_executor_attachment_retirement_with_guard(
2092                self.witness.clone(),
2093                guard,
2094                self.cleanup_spawner.clone(),
2095            );
2096        self.armed = false;
2097    }
2098}
2099
2100impl std::fmt::Debug for PendingRuntimeExecutorAttachment {
2101    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2102        formatter
2103            .debug_struct("PendingRuntimeExecutorAttachment")
2104            .field("witness", &self.witness)
2105            .field("should_wake", &self.should_wake)
2106            .field("armed", &self.armed)
2107            .finish()
2108    }
2109}
2110
2111impl PendingRuntimeExecutorAttachment {
2112    fn new(
2113        machine: Arc<MeerkatMachine>,
2114        witness: RuntimeExecutorAttachmentWitness,
2115        mutation_guard: crate::tokio::sync::OwnedMutexGuard<()>,
2116        cleanup_spawner: MachineCleanupTaskSpawner,
2117        should_wake: bool,
2118        persist_lifecycle_on_commit: bool,
2119    ) -> Self {
2120        Self {
2121            machine,
2122            witness,
2123            mutation_guard: Some(mutation_guard),
2124            cleanup_spawner,
2125            should_wake,
2126            persist_lifecycle_on_commit,
2127            armed: true,
2128        }
2129    }
2130
2131    pub fn witness(&self) -> &RuntimeExecutorAttachmentWitness {
2132        &self.witness
2133    }
2134
2135    #[doc(hidden)]
2136    pub fn cleanup_task_spawner(&self) -> RuntimeCleanupTaskSpawner {
2137        RuntimeCleanupTaskSpawner {
2138            inner: self.cleanup_spawner.clone(),
2139        }
2140    }
2141
2142    pub async fn commit(self) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError> {
2143        self.commit_with_predecessor_mode(|_| Ok(()), false).await
2144    }
2145
2146    /// Commit this exact attachment as the successor to an explicitly retired
2147    /// predecessor in the same process-local runtime entry.
2148    ///
2149    /// The machine publishes the successor and, while still retaining the
2150    /// session mutation gate, fails waiters for every active predecessor input
2151    /// with
2152    /// [`crate::completion::CompletionWaitError::AttachmentReplaced`]. The
2153    /// predecessor's queued work is durably cancelled; an explicit retry is a
2154    /// new request owned by the successor. Fresh materialization must use
2155    /// [`Self::commit`] instead.
2156    pub async fn commit_replacing_predecessor(
2157        self,
2158    ) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError> {
2159        self.commit_with_predecessor_mode(|_| Ok(()), true).await
2160    }
2161
2162    /// Commit the exact attachment and run one synchronous surface publication
2163    /// hook at the same linearization point, before the machine slot becomes
2164    /// observable as serving and before queued work is released.
2165    ///
2166    /// The hook must not call back into `MeerkatMachine`; it is intended for
2167    /// infallible local publication such as flipping an already-installed
2168    /// sidecar's active bit at the same linearization point.
2169    pub async fn commit_with<F>(
2170        self,
2171        on_committed: F,
2172    ) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError>
2173    where
2174        F: FnOnce(&RuntimeExecutorAttachmentWitness) -> Result<(), RuntimeDriverError>
2175            + Send
2176            + 'static,
2177    {
2178        self.commit_with_predecessor_mode(on_committed, false).await
2179    }
2180
2181    /// Commit a replacement attachment and synchronously activate one
2182    /// preinstalled, witness-tagged sidecar at the serving linearization point.
2183    ///
2184    /// This is the replacement sibling of [`Self::commit_with`]. The hook must
2185    /// remain infallible/local and must not call back into `MeerkatMachine`;
2186    /// asynchronous map publication belongs outside M. Before the hook runs,
2187    /// every request owned by the predecessor is terminalized and fenced from
2188    /// the replacement attachment.
2189    pub async fn commit_with_replacing_predecessor<F>(
2190        self,
2191        on_committed: F,
2192    ) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError>
2193    where
2194        F: FnOnce(&RuntimeExecutorAttachmentWitness) -> Result<(), RuntimeDriverError>
2195            + Send
2196            + 'static,
2197    {
2198        self.commit_with_predecessor_mode(on_committed, true).await
2199    }
2200
2201    async fn commit_with_predecessor_mode<F>(
2202        self,
2203        on_committed: F,
2204        replaces_predecessor: bool,
2205    ) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError>
2206    where
2207        F: FnOnce(&RuntimeExecutorAttachmentWitness) -> Result<(), RuntimeDriverError>
2208            + Send
2209            + 'static,
2210    {
2211        let cleanup_spawner = self.cleanup_spawner.clone();
2212        let completion = cleanup_spawner.spawn(async move {
2213            let mut pending = self;
2214            let result = pending
2215                .try_commit_with(on_committed, false, replaces_predecessor)
2216                .await;
2217            match result {
2218                Ok(witness) => Ok(witness),
2219                Err(commit_error) => match pending.abort().await {
2220                    Ok(()) => Err(commit_error),
2221                    Err(cleanup_error) => Err(RuntimeDriverError::Internal(format!(
2222                        "{commit_error}; exact attachment cleanup also failed: {cleanup_error}"
2223                    ))),
2224                },
2225            }
2226        });
2227        completion.await.map_err(|error| {
2228            RuntimeDriverError::Internal(format!(
2229                "owned runtime attachment commit ended without a result: {error}"
2230            ))
2231        })?
2232    }
2233
2234    /// Attempt the final attachment publication while retaining this lease's
2235    /// exact mutation fence on rejection.
2236    ///
2237    /// This is the boundary-owned transaction seam. The caller already owns
2238    /// the service turn-finalization boundary, so a failed attempt can invoke
2239    /// [`Self::abort_under_runtime_turn_finalization_boundary`] without ever
2240    /// opening a B/M gap. Cancellation leaves this lease armed; its `Drop`
2241    /// starts ordinary exact cleanup after the caller's boundary is released.
2242    /// When `replaces_predecessor` is true, every active predecessor input is
2243    /// durably cancelled and its waiter receives
2244    /// [`crate::completion::CompletionWaitError::AttachmentReplaced`] before B
2245    /// becomes visible. No queued request is transferred between attachments.
2246    pub async fn try_commit_with_under_runtime_turn_finalization_boundary<F>(
2247        &mut self,
2248        replaces_predecessor: bool,
2249        on_committed: F,
2250    ) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError>
2251    where
2252        F: FnOnce(&RuntimeExecutorAttachmentWitness) -> Result<(), RuntimeDriverError>,
2253    {
2254        self.try_commit_with(on_committed, false, replaces_predecessor)
2255            .await
2256    }
2257
2258    /// Retain durable lifecycle commit, this exact M guard, and the
2259    /// runtime-loop serving token for a later surface publication decision.
2260    /// The sidecar and runtime loop remain non-admitting until the returned
2261    /// lease is committed.
2262    pub async fn try_commit_with_retained_publication_lease_under_runtime_turn_finalization_boundary(
2263        &mut self,
2264    ) -> Result<CommittedRuntimeExecutorAttachmentPublicationLease, RuntimeDriverError> {
2265        let witness = self.try_commit_with(|_| Ok(()), true, false).await?;
2266        let mutation_guard = self.mutation_guard.take().ok_or_else(|| {
2267            RuntimeDriverError::Internal(
2268                "committed attachment lost its retained publication mutation fence".to_string(),
2269            )
2270        })?;
2271        // `try_commit_with` disarms Pending on success; transfer the exact
2272        // guard into the publication lease instead of releasing it.
2273        Ok(CommittedRuntimeExecutorAttachmentPublicationLease {
2274            machine: Arc::clone(&self.machine),
2275            witness,
2276            mutation_guard: Some(mutation_guard),
2277            cleanup_spawner: self.cleanup_spawner.clone(),
2278            should_wake: self.should_wake,
2279            persist_lifecycle_on_commit: self.persist_lifecycle_on_commit,
2280            armed: true,
2281        })
2282    }
2283
2284    async fn try_commit_with<F>(
2285        &mut self,
2286        on_committed: F,
2287        retain_mutation_guard: bool,
2288        replaces_predecessor: bool,
2289    ) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError>
2290    where
2291        F: FnOnce(&RuntimeExecutorAttachmentWitness) -> Result<(), RuntimeDriverError>,
2292    {
2293        let guard = self.mutation_guard.as_ref().ok_or_else(|| {
2294            RuntimeDriverError::Internal(
2295                "pending runtime attachment lost its mutation fence before commit".to_string(),
2296            )
2297        })?;
2298        if retain_mutation_guard && replaces_predecessor {
2299            return Err(RuntimeDriverError::Internal(
2300                "predecessor request terminalization belongs to final serving publication"
2301                    .to_string(),
2302            ));
2303        }
2304        let predecessor_request_state = if replaces_predecessor {
2305            let sessions = self.machine.sessions.read().await;
2306            let entry =
2307                sessions
2308                    .get(self.witness.session_id())
2309                    .ok_or(RuntimeDriverError::NotReady {
2310                        state: RuntimeState::Destroyed,
2311                    })?;
2312            let exact_pending = entry.epoch_id == self.witness.epoch_id
2313                && matches!(
2314                    &entry.attachment_slot,
2315                    RuntimeLoopAttachmentSlot::Pending(attachment)
2316                        if attachment.id == self.witness.attachment_id
2317                            && !attachment.wake_tx.is_closed()
2318                            && !attachment.effect_tx.is_closed()
2319                );
2320            if !exact_pending {
2321                return Err(RuntimeDriverError::StaleAuthority {
2322                    reason: format!(
2323                        "replacement attachment for session {} changed before waiter fencing",
2324                        self.witness.session_id()
2325                    ),
2326                });
2327            }
2328            Some((
2329                entry.driver.clone(),
2330                entry.completions.clone(),
2331                entry.publication_handle(),
2332            ))
2333        } else {
2334            None
2335        };
2336        let mut predecessor_terminal_publication = None;
2337        if let Some((driver, completions, publication_handle)) = predecessor_request_state {
2338            const REPLACED_REASON: &str =
2339                "executor attachment was replaced before request completion";
2340            let (predecessor_input_ids, candidate_owner_input_id) = {
2341                let mut driver = driver.lock().await;
2342                let predecessor_input_ids = driver.as_driver().active_input_ids();
2343                if predecessor_input_ids.is_empty() {
2344                    (predecessor_input_ids, None)
2345                } else {
2346                    // The outbox candidates and Cancelled input terminals are
2347                    // persisted by one driver checkpoint. Replacement may
2348                    // later fail, but A's requests remain validly terminal and
2349                    // a future attachment can replay any unpublished terminal.
2350                    let prepared = driver.prepare_runless_runtime_terminated_interaction_outboxes(
2351                        &predecessor_input_ids,
2352                        REPLACED_REASON.to_string(),
2353                    )?;
2354                    if let Err(error) = driver
2355                        .abandon_pending_inputs(crate::input_state::InputAbandonReason::Cancelled)
2356                        .await
2357                    {
2358                        driver.rollback_prepared_runless_interaction_terminal_outboxes(prepared);
2359                        return Err(error);
2360                    }
2361                    let candidate_owner_input_id = crate::meerkat_machine::driver::DriverEntry::commit_prepared_runless_interaction_terminal_outboxes(prepared);
2362                    (predecessor_input_ids, candidate_owner_input_id)
2363                }
2364            };
2365            if !predecessor_input_ids.is_empty() {
2366                // M has been retained since B was installed Pending, so this
2367                // exact snapshot is the full A-era recipient set: no B waiter
2368                // can register yet. Fail only those recipients and stage
2369                // their already-durable terminal outboxes for publication
2370                // after M is released, without allowing B to report A's work
2371                // as a successful completion.
2372                completions.lock().await.fail_inputs(
2373                    predecessor_input_ids.iter().cloned(),
2374                    crate::completion::CompletionWaitError::AttachmentReplaced,
2375                );
2376                self.should_wake = false;
2377                if let Some(candidate_owner_input_id) = candidate_owner_input_id {
2378                    predecessor_terminal_publication = Some((
2379                        driver,
2380                        publication_handle,
2381                        predecessor_input_ids,
2382                        candidate_owner_input_id,
2383                    ));
2384                }
2385            }
2386        }
2387        let result = self
2388            .machine
2389            .commit_pending_executor_attachment(
2390                &self.witness,
2391                guard,
2392                self.should_wake,
2393                self.persist_lifecycle_on_commit,
2394                retain_mutation_guard,
2395                on_committed,
2396            )
2397            .await;
2398        if result.is_ok() {
2399            if !retain_mutation_guard {
2400                self.mutation_guard.take();
2401            }
2402            self.armed = false;
2403            if let Some((driver, publication_handle, input_ids, candidate_owner_input_id)) =
2404                predecessor_terminal_publication
2405            {
2406                // External publication must never pin M or delay B's serving
2407                // linearization. The terminal/input rows above are already
2408                // durable; this bounded best-effort handoff records receipts
2409                // after M is released. Failure leaves the exact outbox for a
2410                // later attachment's ordinary startup recovery.
2411                let _terminal_publication = self.cleanup_spawner.spawn(async move {
2412                    let deadline = Instant::now() + std::time::Duration::from_secs(5);
2413                    if let Err(error) = crate::control_plane::publish_and_resolve_runless_runtime_termination_before(
2414                        &driver,
2415                        None,
2416                        publication_handle.as_deref(),
2417                        &input_ids,
2418                        Some(&candidate_owner_input_id),
2419                        "executor attachment was replaced before request completion",
2420                        Some(deadline),
2421                    )
2422                    .await
2423                    {
2424                        tracing::warn!(
2425                            %error,
2426                            "replacement terminal publication remains durably pending"
2427                        );
2428                    }
2429                });
2430            }
2431        }
2432        result
2433    }
2434
2435    pub async fn abort(mut self) -> Result<(), RuntimeDriverError> {
2436        let guard = self.mutation_guard.take().ok_or_else(|| {
2437            RuntimeDriverError::Internal(
2438                "pending runtime attachment lost its mutation fence before abort".to_string(),
2439            )
2440        })?;
2441        let machine = Arc::clone(&self.machine);
2442        let witness = self.witness.clone();
2443        let completion = self.cleanup_spawner.spawn(async move {
2444            machine
2445                .abort_pending_executor_attachment(witness, guard)
2446                .await
2447        });
2448        self.armed = false;
2449        completion.await.map_err(|error| {
2450            RuntimeDriverError::Internal(format!(
2451                "owned runtime attachment abort ended without a result: {error}"
2452            ))
2453        })?
2454    }
2455
2456    /// Abort while the caller owns the stable session turn boundary. The
2457    /// stored attachment-local cleanup handle is completed through its
2458    /// non-reentrant variant before the exact unregister saga runs.
2459    pub async fn abort_under_runtime_turn_finalization_boundary(
2460        mut self,
2461    ) -> Result<(), RuntimeDriverError> {
2462        self.machine
2463            .complete_executor_attachment_cleanup_under_runtime_turn_boundary(&self.witness)
2464            .await?;
2465        let guard = self.mutation_guard.take().ok_or_else(|| {
2466            RuntimeDriverError::Internal(
2467                "pending runtime attachment lost its mutation fence before boundary-owned abort"
2468                    .to_string(),
2469            )
2470        })?;
2471        let machine = Arc::clone(&self.machine);
2472        let witness = self.witness.clone();
2473        let completion = self.cleanup_spawner.spawn(async move {
2474            machine
2475                .abort_pending_executor_attachment(witness, guard)
2476                .await
2477        });
2478        self.armed = false;
2479        completion.await.map_err(|error| {
2480            RuntimeDriverError::Internal(format!(
2481                "owned boundary-aware runtime attachment abort ended without a result: {error}"
2482            ))
2483        })?
2484    }
2485}
2486
2487impl CommittedRuntimeExecutorAttachmentPublicationLease {
2488    pub fn witness(&self) -> &RuntimeExecutorAttachmentWitness {
2489        &self.witness
2490    }
2491
2492    #[doc(hidden)]
2493    pub fn cleanup_task_spawner(&self) -> RuntimeCleanupTaskSpawner {
2494        RuntimeCleanupTaskSpawner {
2495            inner: self.cleanup_spawner.clone(),
2496        }
2497    }
2498
2499    /// Terminalize work recovered from an interrupted predecessor before this
2500    /// replacement attachment is allowed to serve.
2501    ///
2502    /// This is the narrow member-host revival seam. The retained mutation gate
2503    /// and the pending serving barrier make the driver's active-input snapshot
2504    /// the complete predecessor-owned request set: no successor request can be
2505    /// admitted yet. The inputs are durably abandoned and any process-local
2506    /// waiters fail mechanically with `AttachmentReplaced`, but no interaction
2507    /// terminal outbox is created. A remote controller therefore follows its
2508    /// existing typed timeout path instead of observing a terminal fabricated
2509    /// by replacement attachment B.
2510    pub async fn abandon_recovered_predecessor_inputs(
2511        &mut self,
2512    ) -> Result<usize, RuntimeDriverError> {
2513        let _guard = self.mutation_guard.as_ref().ok_or_else(|| {
2514            RuntimeDriverError::Internal(
2515                "retained attachment lost its mutation fence before predecessor abandonment"
2516                    .to_string(),
2517            )
2518        })?;
2519        let (driver, completions) = {
2520            let sessions = self.machine.sessions.read().await;
2521            let entry = sessions.get(self.witness.session_id()).ok_or_else(|| {
2522                RuntimeDriverError::StaleAuthority {
2523                    reason: format!(
2524                        "recovered predecessor session {} disappeared before abandonment",
2525                        self.witness.session_id()
2526                    ),
2527                }
2528            })?;
2529            let exact_pending = entry.epoch_id == self.witness.epoch_id
2530                && matches!(
2531                    &entry.attachment_slot,
2532                    RuntimeLoopAttachmentSlot::Pending(attachment)
2533                        if attachment.id == self.witness.attachment_id
2534                            && !attachment.wake_tx.is_closed()
2535                            && !attachment.effect_tx.is_closed()
2536                );
2537            if !exact_pending {
2538                return Err(RuntimeDriverError::StaleAuthority {
2539                    reason: format!(
2540                        "recovered predecessor attachment for session {} changed before abandonment",
2541                        self.witness.session_id()
2542                    ),
2543                });
2544            }
2545            (entry.driver.clone(), entry.completions.clone())
2546        };
2547
2548        let predecessor_input_ids = {
2549            let mut driver = driver.lock().await;
2550            let predecessor_input_ids = driver.as_driver().active_input_ids();
2551            if predecessor_input_ids.is_empty() {
2552                return Ok(0);
2553            }
2554            let abandoned = driver
2555                .abandon_pending_inputs(crate::input_state::InputAbandonReason::Cancelled)
2556                .await?;
2557            if abandoned != predecessor_input_ids.len() {
2558                return Err(RuntimeDriverError::Internal(format!(
2559                    "recovered predecessor abandonment for session {} terminalized {abandoned} of {} active inputs",
2560                    self.witness.session_id(),
2561                    predecessor_input_ids.len()
2562                )));
2563            }
2564            for input_id in &predecessor_input_ids {
2565                let stored = driver
2566                    .as_driver()
2567                    .stored_input_state(input_id)
2568                    .ok_or_else(|| {
2569                        RuntimeDriverError::Internal(format!(
2570                            "recovered predecessor input {input_id} disappeared after abandonment"
2571                        ))
2572                    })?;
2573                if !matches!(
2574                    stored.seed.terminal_outcome,
2575                    Some(crate::input_state::InputTerminalOutcome::Abandoned {
2576                        reason: crate::input_state::InputAbandonReason::Cancelled,
2577                    })
2578                ) || stored.state.interaction_terminal_outbox.is_some()
2579                {
2580                    return Err(RuntimeDriverError::Internal(format!(
2581                        "recovered predecessor input {input_id} did not reach silent Cancelled abandonment"
2582                    )));
2583                }
2584            }
2585            predecessor_input_ids
2586        };
2587        let abandoned = predecessor_input_ids.len();
2588        completions.lock().await.fail_inputs(
2589            predecessor_input_ids,
2590            crate::completion::CompletionWaitError::AttachmentReplaced,
2591        );
2592        Ok(abandoned)
2593    }
2594
2595    /// Publish the surface's final local state while the exact pending
2596    /// attachment still owns M, then make the attachment serving.
2597    pub async fn commit_with<F>(
2598        mut self,
2599        on_committed: F,
2600    ) -> Result<RuntimeExecutorAttachmentWitness, RuntimeDriverError>
2601    where
2602        F: FnOnce(&RuntimeExecutorAttachmentWitness) -> Result<(), RuntimeDriverError>,
2603    {
2604        let guard = self.mutation_guard.as_ref().ok_or_else(|| {
2605            RuntimeDriverError::Internal(
2606                "retained attachment publication lost its mutation fence".to_string(),
2607            )
2608        })?;
2609        let witness = self
2610            .machine
2611            .commit_retained_executor_attachment_publication(
2612                &self.witness,
2613                guard,
2614                self.should_wake,
2615                self.persist_lifecycle_on_commit,
2616                move |witness, _| on_committed(witness),
2617            )
2618            .await?;
2619        self.mutation_guard.take();
2620        self.armed = false;
2621        Ok(witness)
2622    }
2623
2624    /// Atomically publish the exact host residency and a surface sidecar at
2625    /// the retained M linearization point, then make the runtime loop serving.
2626    pub async fn commit_with_member_residency<F>(
2627        &mut self,
2628        residency_update: MemberResidencyUpdate,
2629        incarnation: meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
2630        tracked_turn_journal: Option<Arc<dyn crate::member_observation::TrackedTurnJournal>>,
2631        on_committed: F,
2632    ) -> Result<(RuntimeExecutorAttachmentWitness, MemberResidencyPublication), RuntimeDriverError>
2633    where
2634        F: FnOnce(&RuntimeExecutorAttachmentWitness) -> Result<(), RuntimeDriverError>,
2635    {
2636        residency_update.validate_for_retained_attachment(
2637            &self.machine,
2638            &self.witness,
2639            &incarnation,
2640        )?;
2641        let guard = self.mutation_guard.as_ref().ok_or_else(|| {
2642            RuntimeDriverError::Internal(
2643                "retained host publication lost its mutation fence".to_string(),
2644            )
2645        })?;
2646        let mut staged_residency_publication = None;
2647        let commit = self
2648            .machine
2649            .commit_retained_executor_attachment_publication(
2650                &self.witness,
2651                guard,
2652                self.should_wake,
2653                self.persist_lifecycle_on_commit,
2654                |witness, session_mutation_gate| {
2655                    on_committed(witness)?;
2656                    staged_residency_publication =
2657                        Some(residency_update.stage_under_retained_mutation_gate(
2658                            incarnation,
2659                            tracked_turn_journal,
2660                            session_mutation_gate,
2661                        )?);
2662                    Ok(())
2663                },
2664            )
2665            .await;
2666        let witness = match commit {
2667            Ok(witness) => witness,
2668            Err(error) => {
2669                // Roll residency back to VacantPlaced while this lease still
2670                // retains the exact M guard. The staged value also retains the
2671                // stable residency-slot guard until rollback is complete.
2672                drop(staged_residency_publication);
2673                return Err(error);
2674            }
2675        };
2676        let residency_publication = staged_residency_publication
2677            .take()
2678            .ok_or_else(|| {
2679                RuntimeDriverError::Internal(format!(
2680                    "retained host publication for session {} committed without residency",
2681                    self.witness.session_id()
2682                ))
2683            })?
2684            .finalize()?;
2685        self.mutation_guard.take();
2686        self.armed = false;
2687        Ok((witness, residency_publication))
2688    }
2689
2690    /// Transfer this exact retained attachment into the canonical unregister
2691    /// saga without waiting for service-owned post-stop cleanup.
2692    ///
2693    /// A caller that already owns the service turn-finalization boundary must
2694    /// release that boundary before awaiting the returned completion. The
2695    /// machine first commits Draining and terminalizes the executor; only then
2696    /// does its attachment-local cleanup callback reacquire the service
2697    /// boundary and remove the actor/sidecar assembly.
2698    pub fn begin_abort(
2699        mut self,
2700    ) -> Result<RuntimeExecutorAttachmentRetirementCompletion, RuntimeDriverError> {
2701        let guard = self.mutation_guard.take().ok_or_else(|| {
2702            RuntimeDriverError::Internal(
2703                "committed publication lease lost its mutation fence before abort".to_string(),
2704            )
2705        })?;
2706        let completion = self
2707            .machine
2708            .spawn_executor_attachment_retirement_with_guard(
2709                self.witness.clone(),
2710                guard,
2711                self.cleanup_spawner.clone(),
2712            );
2713        self.armed = false;
2714        Ok(completion)
2715    }
2716
2717    /// Retire this exact retained attachment while the surface retains B.
2718    pub async fn abort_under_runtime_turn_finalization_boundary(
2719        mut self,
2720    ) -> Result<bool, RuntimeDriverError> {
2721        self.machine
2722            .complete_executor_attachment_cleanup_under_runtime_turn_boundary(&self.witness)
2723            .await?;
2724        let guard = self.mutation_guard.take().ok_or_else(|| {
2725            RuntimeDriverError::Internal(
2726                "committed publication lease lost its mutation fence before abort".to_string(),
2727            )
2728        })?;
2729        let machine = Arc::clone(&self.machine);
2730        let witness = self.witness.clone();
2731        let completion = self.cleanup_spawner.spawn(async move {
2732            machine
2733                .unregister_executor_attachment_if_current_with_guard(witness, guard)
2734                .await
2735        });
2736        self.armed = false;
2737        completion.await.map_err(|error| {
2738            RuntimeDriverError::Internal(format!(
2739                "owned committed attachment abort ended without a result: {error}"
2740            ))
2741        })?
2742    }
2743}
2744
2745impl Drop for CommittedRuntimeExecutorAttachmentPublicationLease {
2746    fn drop(&mut self) {
2747        if !self.armed {
2748            return;
2749        }
2750        let Some(guard) = self.mutation_guard.take() else {
2751            return;
2752        };
2753        self.armed = false;
2754        let machine = Arc::clone(&self.machine);
2755        let witness = self.witness.clone();
2756        self.cleanup_spawner.spawn(async move {
2757            if let Err(error) = machine
2758                .unregister_executor_attachment_if_current_with_guard(witness.clone(), guard)
2759                .await
2760            {
2761                tracing::warn!(
2762                    session_id = %witness.session_id(),
2763                    %error,
2764                    "drop-time committed attachment publication abort failed"
2765                );
2766            }
2767        });
2768    }
2769}
2770
2771impl Drop for PendingRuntimeExecutorAttachment {
2772    fn drop(&mut self) {
2773        if !self.armed {
2774            return;
2775        }
2776        let Some(guard) = self.mutation_guard.take() else {
2777            return;
2778        };
2779        self.armed = false;
2780        let machine = Arc::clone(&self.machine);
2781        let witness = self.witness.clone();
2782        self.cleanup_spawner.spawn(async move {
2783            if let Err(error) = machine
2784                .abort_pending_executor_attachment(witness.clone(), guard)
2785                .await
2786            {
2787                tracing::warn!(
2788                    session_id = %witness.session_id(),
2789                    %error,
2790                    "drop-time exact runtime attachment abort failed"
2791                );
2792            }
2793        });
2794    }
2795}
2796
2797impl std::fmt::Debug for PreparedSessionMaterialization {
2798    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2799        formatter
2800            .debug_struct("PreparedSessionMaterialization")
2801            .field("session_id", self.bindings.session_id())
2802            .field("epoch_id", self.bindings.epoch_id())
2803            .field("claim_id", &self.claim_id)
2804            .field("armed", &self.armed)
2805            .finish()
2806    }
2807}
2808
2809impl std::fmt::Debug for PreparedAttachedSessionActorRecovery {
2810    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2811        formatter
2812            .debug_struct("PreparedAttachedSessionActorRecovery")
2813            .field("session_id", self.bindings.session_id())
2814            .field("epoch_id", self.bindings.epoch_id())
2815            .field("witness", &self.witness)
2816            .field("armed", &self.armed)
2817            .finish()
2818    }
2819}
2820
2821impl PreparedAttachedSessionActorRecovery {
2822    fn new(
2823        bindings: meerkat_core::SessionRuntimeBindings,
2824        witness: RuntimeExecutorAttachmentWitness,
2825        claim_id: uuid::Uuid,
2826        claim_state: Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>,
2827        previous_phase: crate::RuntimeActorMaterializationClaimPhase,
2828        mutation_guard: crate::tokio::sync::OwnedMutexGuard<()>,
2829    ) -> Self {
2830        Self {
2831            bindings,
2832            witness,
2833            claim_id,
2834            claim_state,
2835            previous_phase,
2836            mutation_guard: Some(mutation_guard),
2837            armed: true,
2838        }
2839    }
2840
2841    pub fn bindings(&self) -> &meerkat_core::SessionRuntimeBindings {
2842        &self.bindings
2843    }
2844
2845    pub fn bindings_clone(&self) -> meerkat_core::SessionRuntimeBindings {
2846        self.bindings.clone()
2847    }
2848
2849    pub fn witness(&self) -> &RuntimeExecutorAttachmentWitness {
2850        &self.witness
2851    }
2852
2853    /// Commit successful actor reconstruction while preserving the same exact
2854    /// serving executor attachment.
2855    pub fn commit_actor(&mut self) -> Result<(), RuntimeDriverError> {
2856        if !self.armed {
2857            return Err(RuntimeDriverError::StaleAuthority {
2858                reason: "attached actor-recovery lease is no longer active".to_string(),
2859            });
2860        }
2861        let changed = {
2862            let mut state = self
2863                .claim_state
2864                .lock()
2865                .unwrap_or_else(std::sync::PoisonError::into_inner);
2866            if !state.exact_claim_is(
2867                self.claim_id,
2868                &[crate::RuntimeActorMaterializationClaimPhase::ActorMaterializedPendingCommit],
2869            ) {
2870                return Err(RuntimeDriverError::StaleAuthority {
2871                    reason: format!(
2872                        "actor recovery for session {} did not retain its exact materialization claim",
2873                        self.bindings.session_id()
2874                    ),
2875                });
2876            }
2877            state.current = None;
2878            state.phase = crate::RuntimeActorMaterializationClaimPhase::RetainedActor;
2879            Arc::clone(&state.changed)
2880        };
2881        changed.notify_waiters();
2882        self.armed = false;
2883        self.mutation_guard.take();
2884        Ok(())
2885    }
2886}
2887
2888impl Drop for PreparedAttachedSessionActorRecovery {
2889    fn drop(&mut self) {
2890        if !self.armed {
2891            return;
2892        }
2893        let changed = {
2894            let mut state = self
2895                .claim_state
2896                .lock()
2897                .unwrap_or_else(std::sync::PoisonError::into_inner);
2898            if state.current == Some(self.claim_id) {
2899                state.current = None;
2900                state.phase = if state.phase
2901                    == crate::RuntimeActorMaterializationClaimPhase::ActorMaterializedPendingCommit
2902                {
2903                    crate::RuntimeActorMaterializationClaimPhase::RetainedActor
2904                } else {
2905                    self.previous_phase
2906                };
2907                Some(Arc::clone(&state.changed))
2908            } else {
2909                None
2910            }
2911        };
2912        if let Some(changed) = changed {
2913            changed.notify_waiters();
2914        }
2915        self.armed = false;
2916        self.mutation_guard.take();
2917    }
2918}
2919
2920impl PreparedSessionMaterialization {
2921    fn new(
2922        machine: Arc<MeerkatMachine>,
2923        bindings: meerkat_core::SessionRuntimeBindings,
2924        claim_id: uuid::Uuid,
2925        cleanup_spawner: MachineCleanupTaskSpawner,
2926    ) -> Result<Self, RuntimeDriverError> {
2927        let authority = bindings
2928            .__runtime_authority()
2929            .downcast_ref::<crate::SessionRuntimeBindingsAuthority>()
2930            .ok_or_else(|| RuntimeDriverError::ValidationFailed {
2931                reason: "prepared materialization lacks MeerkatMachine authority".to_string(),
2932            })?;
2933        if authority.materialization_claim_id != Some(claim_id) {
2934            return Err(RuntimeDriverError::StaleAuthority {
2935                reason: "prepared materialization binding lost its exact claim".to_string(),
2936            });
2937        }
2938        let claim_state = Arc::clone(&authority.materialization_claim_state);
2939        Ok(Self {
2940            machine,
2941            bindings,
2942            claim_id,
2943            claim_state,
2944            cleanup_spawner,
2945            archived_resume_authorization_issued: false,
2946            armed: true,
2947        })
2948    }
2949
2950    pub fn bindings(&self) -> &meerkat_core::SessionRuntimeBindings {
2951        &self.bindings
2952    }
2953
2954    pub fn bindings_clone(&self) -> meerkat_core::SessionRuntimeBindings {
2955        self.bindings.clone()
2956    }
2957
2958    pub fn session_id(&self) -> &SessionId {
2959        self.bindings.session_id()
2960    }
2961
2962    #[doc(hidden)]
2963    pub fn cleanup_task_spawner(&self) -> RuntimeCleanupTaskSpawner {
2964        RuntimeCleanupTaskSpawner {
2965            inner: self.cleanup_spawner.clone(),
2966        }
2967    }
2968
2969    /// Whether this lease still owns the exact process-local materialization
2970    /// claim that fences same-session actor creation and executor attachment.
2971    #[must_use]
2972    pub async fn owns_current_materialization_claim(&self) -> bool {
2973        if !self.armed {
2974            return false;
2975        }
2976        let Some(_mutation_guard) = self
2977            .machine
2978            .lock_current_session_mutation_gate(self.session_id())
2979            .await
2980        else {
2981            return false;
2982        };
2983        let Ok(authority) = crate::validated_session_runtime_bindings_authority(&self.bindings)
2984        else {
2985            return false;
2986        };
2987        let sessions = self.machine.sessions.read().await;
2988        sessions.get(self.session_id()).is_some_and(|entry| {
2989            entry.epoch_id == *self.bindings.epoch_id()
2990                && Arc::ptr_eq(&entry.materialization_claim_state, &self.claim_state)
2991                && Arc::ptr_eq(&entry.dsl_authority, &authority.dsl_authority)
2992                && Arc::ptr_eq(&entry.handle_teardown_gate, &authority.teardown_gate)
2993                && !entry.physical_attachment_is_live()
2994                && self
2995                    .claim_state
2996                    .lock()
2997                    .unwrap_or_else(std::sync::PoisonError::into_inner)
2998                    .exact_claim_is(
2999                        self.claim_id,
3000                        &[
3001                            crate::RuntimeActorMaterializationClaimPhase::Prepared,
3002                            crate::RuntimeActorMaterializationClaimPhase::Staged,
3003                            crate::RuntimeActorMaterializationClaimPhase::ActorCreating,
3004                            crate::RuntimeActorMaterializationClaimPhase::ActorMaterializedPendingCommit,
3005                            crate::RuntimeActorMaterializationClaimPhase::Aborting,
3006                        ],
3007                    )
3008        })
3009    }
3010
3011    /// Mint the non-cloneable authorization for actor construction at an
3012    /// exact Archived+Retired revival midpoint.
3013    ///
3014    /// The returned token is useful only while this prepared lease still owns
3015    /// the same claim. Consumption revalidates the current machine entry,
3016    /// provisional cleanup installation, and exact Retired phase under the
3017    /// session mutation gate.
3018    pub fn archived_resume_authorization(
3019        &mut self,
3020    ) -> Result<ArchivedSessionActorMaterializationAuthorization, RuntimeDriverError> {
3021        if !self.armed {
3022            return Err(RuntimeDriverError::StaleAuthority {
3023                reason: format!(
3024                    "prepared materialization for session {} no longer owns archived-resume authority",
3025                    self.session_id()
3026                ),
3027            });
3028        }
3029        if self.archived_resume_authorization_issued {
3030            return Err(RuntimeDriverError::StaleAuthority {
3031                reason: format!(
3032                    "prepared materialization for session {} already issued its one-shot archived-resume authorization",
3033                    self.session_id()
3034                ),
3035            });
3036        }
3037        let authority = self
3038            .bindings
3039            .__runtime_authority()
3040            .downcast_ref::<crate::SessionRuntimeBindingsAuthority>()
3041            .ok_or_else(|| RuntimeDriverError::ValidationFailed {
3042                reason: "prepared archived-resume materialization lacks machine authority"
3043                    .to_string(),
3044            })?;
3045        if authority.materialization_claim_id != Some(self.claim_id)
3046            || !Arc::ptr_eq(&authority.materialization_claim_state, &self.claim_state)
3047        {
3048            return Err(RuntimeDriverError::StaleAuthority {
3049                reason: format!(
3050                    "prepared archived-resume claim for session {} is no longer exact",
3051                    self.session_id()
3052                ),
3053            });
3054        }
3055        let authorization = ArchivedSessionActorMaterializationAuthorization {
3056            machine: Arc::clone(&self.machine),
3057            session_id: self.session_id().clone(),
3058            epoch_id: self.bindings.epoch_id().clone(),
3059            claim_id: self.claim_id,
3060            claim_state: Arc::clone(&self.claim_state),
3061            dsl_authority: Arc::clone(&authority.dsl_authority),
3062            teardown_gate: Arc::clone(&authority.teardown_gate),
3063        };
3064        self.archived_resume_authorization_issued = true;
3065        Ok(authorization)
3066    }
3067
3068    /// Acquire the exact post-actor commit lease for archived revival.
3069    /// Actor construction must already have consumed the one-shot
3070    /// authorization and committed the exact claim into
3071    /// `ActorMaterializedPendingCommit`.
3072    pub async fn acquire_archived_resume_commit_lease(
3073        &self,
3074    ) -> Result<PreparedArchivedResumeCommitLease, RuntimeDriverError> {
3075        if !self.armed || !self.archived_resume_authorization_issued {
3076            return Err(RuntimeDriverError::StaleAuthority {
3077                reason: format!(
3078                    "prepared materialization for session {} does not own post-actor archived-resume authority",
3079                    self.session_id()
3080                ),
3081            });
3082        }
3083        let mutation_guard = self
3084            .machine
3085            .lock_current_session_mutation_gate(self.session_id())
3086            .await
3087            .ok_or_else(|| RuntimeDriverError::StaleAuthority {
3088                reason: format!(
3089                    "archived-resume session {} disappeared before commit lease acquisition",
3090                    self.session_id()
3091                ),
3092            })?;
3093        let exact = {
3094            let sessions = self.machine.sessions.read().await;
3095            sessions.get(self.session_id()).is_some_and(|entry| {
3096                entry.epoch_id == *self.bindings.epoch_id()
3097                    && Arc::ptr_eq(&entry.materialization_claim_state, &self.claim_state)
3098                    && !entry.physical_attachment_is_live()
3099                    && matches!(
3100                        entry.control_snapshot().phase,
3101                        RuntimeState::Retired | RuntimeState::Idle
3102                    )
3103                    && self
3104                        .claim_state
3105                        .lock()
3106                        .unwrap_or_else(std::sync::PoisonError::into_inner)
3107                        .exact_claim_is(
3108                            self.claim_id,
3109                            &[crate::RuntimeActorMaterializationClaimPhase::ActorMaterializedPendingCommit],
3110                        )
3111            })
3112        };
3113        if !exact {
3114            return Err(RuntimeDriverError::StaleAuthority {
3115                reason: format!(
3116                    "archived-resume session {} lost its exact quiescent post-actor claim",
3117                    self.session_id()
3118                ),
3119            });
3120        }
3121        Ok(PreparedArchivedResumeCommitLease {
3122            machine: Arc::clone(&self.machine),
3123            session_id: self.session_id().clone(),
3124            epoch_id: self.bindings.epoch_id().clone(),
3125            claim_id: self.claim_id,
3126            claim_state: Arc::clone(&self.claim_state),
3127            _mutation_guard: mutation_guard,
3128        })
3129    }
3130
3131    /// Attach the executor that consumes this exact actor-materialization
3132    /// claim. Generic machine attachment is intentionally unable to cross an
3133    /// outstanding prepare/actor-create transaction; this method transfers
3134    /// that authority to the returned pending attachment lease.
3135    pub async fn ensure_executor_attachment<F>(
3136        &mut self,
3137        executor_factory: F,
3138    ) -> Result<EnsureRuntimeExecutorAttachment, RuntimeDriverError>
3139    where
3140        F: FnOnce(
3141                RuntimeExecutorAttachmentWitness,
3142            ) -> Box<dyn meerkat_core::lifecycle::CoreExecutor>
3143            + Send
3144            + 'static,
3145    {
3146        self.ensure_executor_attachment_with_boundary_mode(executor_factory, false)
3147            .await
3148    }
3149
3150    /// Attach the executor while the shared session facade already owns the
3151    /// service turn-finalization boundary.
3152    ///
3153    /// Startup rejection must use the non-reentrant post-stop cleanup path in
3154    /// this mode. Otherwise an unserved attachment can begin ordinary cleanup
3155    /// while retaining M and deadlock trying to reacquire the caller's B.
3156    #[doc(hidden)]
3157    pub async fn ensure_executor_attachment_under_runtime_turn_finalization_boundary<F>(
3158        &mut self,
3159        executor_factory: F,
3160    ) -> Result<EnsureRuntimeExecutorAttachment, RuntimeDriverError>
3161    where
3162        F: FnOnce(
3163                RuntimeExecutorAttachmentWitness,
3164            ) -> Box<dyn meerkat_core::lifecycle::CoreExecutor>
3165            + Send
3166            + 'static,
3167    {
3168        self.ensure_executor_attachment_with_boundary_mode(executor_factory, true)
3169            .await
3170    }
3171
3172    async fn ensure_executor_attachment_with_boundary_mode<F>(
3173        &mut self,
3174        executor_factory: F,
3175        turn_finalization_boundary_already_held: bool,
3176    ) -> Result<EnsureRuntimeExecutorAttachment, RuntimeDriverError>
3177    where
3178        F: FnOnce(
3179                RuntimeExecutorAttachmentWitness,
3180            ) -> Box<dyn meerkat_core::lifecycle::CoreExecutor>
3181            + Send
3182            + 'static,
3183    {
3184        if !self.armed {
3185            return Err(RuntimeDriverError::StaleAuthority {
3186                reason: format!(
3187                    "prepared materialization for session {} no longer owns attachment authority",
3188                    self.session_id()
3189                ),
3190            });
3191        }
3192        let expected_claim = RuntimeExecutorAttachmentMaterializationClaim {
3193            claim_id: self.claim_id,
3194            claim_state: Arc::clone(&self.claim_state),
3195            epoch_id: self.bindings.epoch_id().clone(),
3196        };
3197        let outcome = self
3198            .machine
3199            .ensure_session_with_executor_factory_for_materialization(
3200                self.session_id().clone(),
3201                expected_claim,
3202                turn_finalization_boundary_already_held,
3203                executor_factory,
3204            )
3205            .await?;
3206        if matches!(&outcome, EnsureRuntimeExecutorAttachment::Pending(_)) {
3207            // The exact machine attachment now owns rollback. Its publication
3208            // cleared this claim synchronously, so dropping this shell must not
3209            // start a second cleanup saga.
3210            self.armed = false;
3211        }
3212        Ok(outcome)
3213    }
3214
3215    fn request_abort(&self) -> bool {
3216        let mut state = self
3217            .claim_state
3218            .lock()
3219            .unwrap_or_else(std::sync::PoisonError::into_inner);
3220        if !state.exact_claim_is(
3221            self.claim_id,
3222            &[
3223                crate::RuntimeActorMaterializationClaimPhase::Prepared,
3224                crate::RuntimeActorMaterializationClaimPhase::Staged,
3225                crate::RuntimeActorMaterializationClaimPhase::ActorCreating,
3226                crate::RuntimeActorMaterializationClaimPhase::ActorMaterializedPendingCommit,
3227                crate::RuntimeActorMaterializationClaimPhase::Aborting,
3228            ],
3229        ) {
3230            return false;
3231        }
3232        state.phase = crate::RuntimeActorMaterializationClaimPhase::Aborting;
3233        true
3234    }
3235
3236    /// Roll back this exact materialization now. A pre-existing runtime entry is
3237    /// preserved after its exact provisional cleanup; an entry inserted for this
3238    /// transaction is fully unregistered.
3239    pub async fn rollback_now(&mut self) -> Result<bool, RuntimeDriverError> {
3240        self.rollback_now_with_turn_finalization_boundary(false)
3241            .await
3242    }
3243
3244    /// Exact rollback for a caller that already owns the persistent service's
3245    /// turn-finalization boundary.
3246    pub async fn rollback_now_under_turn_finalization_boundary(
3247        &mut self,
3248    ) -> Result<bool, RuntimeDriverError> {
3249        self.rollback_now_with_turn_finalization_boundary(true)
3250            .await
3251    }
3252
3253    async fn rollback_now_with_turn_finalization_boundary(
3254        &mut self,
3255        turn_finalization_boundary_already_held: bool,
3256    ) -> Result<bool, RuntimeDriverError> {
3257        if !self.armed {
3258            return Ok(false);
3259        }
3260        if !self.request_abort() {
3261            self.armed = false;
3262            return Ok(false);
3263        }
3264        let rolled_back = self
3265            .machine
3266            .abort_prepared_session_materialization_claim(
3267                self.bindings.session_id(),
3268                self.claim_id,
3269                Some(self.bindings.epoch_id()),
3270                Some(&self.claim_state),
3271                turn_finalization_boundary_already_held,
3272            )
3273            .await?;
3274        self.armed = false;
3275        Ok(rolled_back)
3276    }
3277
3278    /// Publish a staged-session owner. The staged registry must subsequently
3279    /// retain the exact bindings and call exact rollback when it abandons the
3280    /// slot; actor construction may later consume the same claim.
3281    pub async fn commit_staged(&mut self) -> Result<(), RuntimeDriverError> {
3282        self.machine
3283            .commit_prepared_session_materialization_staged(&self.bindings)
3284            .await?;
3285        self.armed = false;
3286        Ok(())
3287    }
3288
3289    /// Retain a successfully created actor before executor attachment. The
3290    /// provisional cleanup stays on the machine entry for raw unregister, while
3291    /// this transaction can no longer roll the registration back.
3292    pub async fn commit_actor_unattached(&mut self) -> Result<(), RuntimeDriverError> {
3293        self.machine
3294            .commit_prepared_session_actor_unattached(&self.bindings)
3295            .await?;
3296        self.armed = false;
3297        Ok(())
3298    }
3299}
3300
3301impl Drop for PreparedSessionMaterialization {
3302    fn drop(&mut self) {
3303        if !self.armed || !self.request_abort() {
3304            return;
3305        }
3306        let machine = Arc::clone(&self.machine);
3307        let session_id = self.bindings.session_id().clone();
3308        let epoch_id = self.bindings.epoch_id().clone();
3309        let claim_id = self.claim_id;
3310        let claim_state = Arc::clone(&self.claim_state);
3311        self.cleanup_spawner.spawn(async move {
3312            if let Err(error) = machine
3313                .abort_prepared_session_materialization_claim(
3314                    &session_id,
3315                    claim_id,
3316                    Some(&epoch_id),
3317                    Some(&claim_state),
3318                    false,
3319                )
3320                .await
3321            {
3322                tracing::warn!(
3323                    %session_id,
3324                    %error,
3325                    "drop-time exact prepared materialization rollback failed"
3326                );
3327            }
3328        });
3329    }
3330}
3331
3332pub(super) struct PendingPreparedMaterialization {
3333    machine: Arc<MeerkatMachine>,
3334    session_id: SessionId,
3335    claim_id: uuid::Uuid,
3336    cleanup_spawner: MachineCleanupTaskSpawner,
3337    claim_state_slot: Arc<
3338        std::sync::Mutex<
3339            Option<Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>>,
3340        >,
3341    >,
3342    armed: bool,
3343}
3344
3345impl PendingPreparedMaterialization {
3346    pub(super) fn new(
3347        machine: Arc<MeerkatMachine>,
3348        session_id: SessionId,
3349        claim_id: uuid::Uuid,
3350    ) -> Result<Self, RuntimeDriverError> {
3351        Ok(Self {
3352            machine,
3353            session_id,
3354            claim_id,
3355            cleanup_spawner: MachineCleanupTaskSpawner::acquire()?,
3356            claim_state_slot: Arc::new(std::sync::Mutex::new(None)),
3357            armed: true,
3358        })
3359    }
3360
3361    fn cleanup_spawner(&self) -> MachineCleanupTaskSpawner {
3362        self.cleanup_spawner.clone()
3363    }
3364
3365    pub(super) fn claim_state_slot(
3366        &self,
3367    ) -> Arc<
3368        std::sync::Mutex<
3369            Option<Arc<std::sync::Mutex<crate::RuntimeActorMaterializationClaimState>>>,
3370        >,
3371    > {
3372        Arc::clone(&self.claim_state_slot)
3373    }
3374
3375    pub(super) fn disarm(&mut self) {
3376        self.armed = false;
3377    }
3378}
3379
3380impl Drop for PendingPreparedMaterialization {
3381    fn drop(&mut self) {
3382        if !self.armed {
3383            return;
3384        }
3385        let claim_state = self
3386            .claim_state_slot
3387            .lock()
3388            .unwrap_or_else(std::sync::PoisonError::into_inner)
3389            .clone();
3390        if let Some(claim_state) = claim_state {
3391            let mut state = claim_state
3392                .lock()
3393                .unwrap_or_else(std::sync::PoisonError::into_inner);
3394            if state.current == Some(self.claim_id)
3395                && state.phase != crate::RuntimeActorMaterializationClaimPhase::RetainedActor
3396            {
3397                state.phase = crate::RuntimeActorMaterializationClaimPhase::Aborting;
3398            }
3399        }
3400        let machine = Arc::clone(&self.machine);
3401        let session_id = self.session_id.clone();
3402        let claim_id = self.claim_id;
3403        self.cleanup_spawner.spawn(async move {
3404            if let Err(error) = machine
3405                .abort_prepared_session_materialization_claim(
3406                    &session_id,
3407                    claim_id,
3408                    None,
3409                    None,
3410                    false,
3411                )
3412                .await
3413            {
3414                tracing::warn!(
3415                    %session_id,
3416                    %error,
3417                    "cancelled binding preparation exact rollback failed"
3418                );
3419            }
3420        });
3421    }
3422}
3423
3424/// Exact same-session mutation lease used to commit a direct SessionService
3425/// turn without reacquiring the machine gate while the session recovery gate
3426/// is held.
3427///
3428/// The session layer acquires this only after releasing its execution-time
3429/// recovery guard, then reacquires recovery in the canonical
3430/// machine-mutation -> recovery order for snapshot commit/checkpoint.
3431pub struct MachineServiceTurnCommitLease {
3432    session_id: SessionId,
3433    driver: SharedDriver,
3434    _mutation_guard: crate::tokio::sync::OwnedMutexGuard<()>,
3435}
3436
3437/// Exact runtime-entry identity captured before a direct SessionService turn.
3438/// It carries no lock; terminal commit later acquires mutation authority only
3439/// if this same driver is still current.
3440pub struct MachineServiceTurnIdentity {
3441    session_id: SessionId,
3442    driver: SharedDriver,
3443}
3444
3445impl MachineSessionArchiveLease {
3446    /// Whether archive preparation inserted the captured in-memory
3447    /// registration from durable authority.
3448    pub fn owns_recovered_registration(&self) -> bool {
3449        self.recovered_registration_for_archive
3450    }
3451
3452    /// Exact runtime lifecycle observed while this lease owns the session's
3453    /// mutation gate.
3454    pub async fn runtime_state(&self) -> RuntimeState {
3455        self.driver.lock().await.runtime_state()
3456    }
3457
3458    /// Whether the captured runtime half has already reached the durable
3459    /// archive terminal. The lease owns the session mutation gate, so this
3460    /// observation cannot race a lifecycle transition.
3461    pub async fn runtime_is_retired(&self) -> bool {
3462        self.runtime_state().await == RuntimeState::Retired
3463    }
3464}
3465
3466/// Capability bundle for an attached runtime loop.
3467///
3468/// Keep all loop-related handles together so "attached vs detached" cannot
3469/// drift into partially-populated shell state.
3470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3471struct RuntimeLoopAttachmentId(uuid::Uuid);
3472
3473impl RuntimeLoopAttachmentId {
3474    fn new() -> Self {
3475        Self(uuid::Uuid::new_v4())
3476    }
3477}
3478
3479/// Mechanical proof captured while M is held for one executor-effect
3480/// dispatch. A callback may release M, but only this exact attachment may
3481/// receive the resulting effect after revalidation.
3482struct RuntimeEffectDispatchAttachmentWitness {
3483    mutation_gate: Arc<Mutex<()>>,
3484    driver: SharedDriver,
3485    dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
3486    attachment_id: RuntimeLoopAttachmentId,
3487    effect_tx: mpsc::Sender<crate::effect::RuntimeEffect>,
3488}
3489
3490/// Exact executor attachment captured before a cooperative live-boundary
3491/// callback. Publication resumes only after every attachment-local component
3492/// is revalidated.
3493struct RuntimeLiveBoundaryAttachmentWitness {
3494    mutation_gate: Arc<Mutex<()>>,
3495    driver: SharedDriver,
3496    dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
3497    attachment_id: RuntimeLoopAttachmentId,
3498    boundary_handle: Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>,
3499}
3500
3501struct RuntimeAcceptedBoundaryCancelPlan {
3502    witness: RuntimeEffectDispatchAttachmentWitness,
3503    boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
3504    pending_dispatch: PendingBoundaryCancelDispatchGuard,
3505    expected_run_id: RunId,
3506    projected_effect: crate::effect::ProjectedRuntimeEffect,
3507    dispatch_generation: u64,
3508    dispatch_lifecycle_phase: dsl::MeerkatPhase,
3509}
3510
3511#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3512enum BoundaryCancelDispatchState {
3513    Current,
3514    ConsumedConverged,
3515    SameGenerationOrphan,
3516    Superseded,
3517}
3518
3519struct RuntimeEffectDispatchMemberAuthority {
3520    lease: MemberEffectAuthorityLease,
3521    expected_member: Option<meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation>,
3522}
3523
3524/// Exact-generation compensation for a boundary dispatch abandoned before
3525/// enqueue. It can only clear the generation it captured.
3526struct PendingBoundaryCancelDispatchGuard {
3527    dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
3528    dispatch_generation: u64,
3529    armed: bool,
3530}
3531
3532impl PendingBoundaryCancelDispatchGuard {
3533    fn new(
3534        dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
3535        dispatch_generation: u64,
3536    ) -> Self {
3537        Self {
3538            dsl_authority,
3539            dispatch_generation,
3540            armed: true,
3541        }
3542    }
3543
3544    fn disarm(&mut self) {
3545        self.armed = false;
3546    }
3547}
3548
3549impl Drop for PendingBoundaryCancelDispatchGuard {
3550    fn drop(&mut self) {
3551        if self.armed
3552            && let Err(error) = MeerkatMachine::abort_dsl_boundary_cancel_dispatch_if_current(
3553                &self.dsl_authority,
3554                self.dispatch_generation,
3555            )
3556        {
3557            tracing::error!(
3558                dispatch_generation = self.dispatch_generation,
3559                error = %error,
3560                "failed to compensate an abandoned boundary-cancel dispatch"
3561            );
3562        }
3563    }
3564}
3565
3566/// Mechanical fallback for a durably accepted input whose later publication
3567/// step exits early. The wake channel belongs to the captured attachment.
3568struct AcceptedIngressFallbackWakeGuard {
3569    wake_tx: Option<mpsc::Sender<()>>,
3570    armed: bool,
3571}
3572
3573impl AcceptedIngressFallbackWakeGuard {
3574    fn new(wake_tx: Option<mpsc::Sender<()>>, armed: bool) -> Self {
3575        Self { wake_tx, armed }
3576    }
3577
3578    fn disarm(&mut self) {
3579        self.armed = false;
3580    }
3581}
3582
3583impl Drop for AcceptedIngressFallbackWakeGuard {
3584    fn drop(&mut self) {
3585        if self.armed
3586            && let Some(wake_tx) = self.wake_tx.as_ref()
3587        {
3588            let _ = wake_tx.try_send(());
3589        }
3590    }
3591}
3592
3593struct RuntimeLoopAttachment {
3594    id: RuntimeLoopAttachmentId,
3595    wake_tx: mpsc::Sender<()>,
3596    effect_tx: mpsc::Sender<crate::effect::RuntimeEffect>,
3597    serving_release: Option<crate::runtime_loop::RuntimeLoopServingRelease>,
3598    boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
3599    interrupt_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
3600    loop_handle: tokio::task::JoinHandle<()>,
3601}
3602
3603/// Mechanical runtime-loop channel slot.
3604enum RuntimeLoopAttachmentSlot {
3605    Empty,
3606    Pending(RuntimeLoopAttachment),
3607    Attached(RuntimeLoopAttachment),
3608}
3609
3610impl RuntimeSessionEntry {
3611    fn dsl_mutation_blocked_by_unregister(
3612        &self,
3613        session_id: &SessionId,
3614    ) -> Option<RuntimeDriverError> {
3615        if self.pending_unregister_finalization.is_some() {
3616            return Some(RuntimeDriverError::UnregisterFinalizationOutcomeUnknown {
3617                reason: format!(
3618                    "session {session_id} retains an ambiguous unregister finalization; retry unregister before applying any other lifecycle mutation"
3619                ),
3620            });
3621        }
3622        self.handle_teardown_gate
3623            .is_closed()
3624            .then(|| RuntimeDriverError::ValidationFailed {
3625                reason: format!("session {session_id} is a teardown-only unregister retry anchor"),
3626            })
3627    }
3628
3629    fn registration_blocked_by_unregister(
3630        &self,
3631        session_id: &SessionId,
3632    ) -> Option<RuntimeDriverError> {
3633        if let Some(error) = self.dsl_mutation_blocked_by_unregister(session_id) {
3634            return Some(error);
3635        }
3636        if self.unregister_coordinator.is_some() {
3637            return Some(RuntimeDriverError::NotReady {
3638                state: self.control_snapshot().phase,
3639            });
3640        }
3641        let Some(coordinator) = self.runtime_stop_cleanup_coordinator.as_ref() else {
3642            return None;
3643        };
3644        match coordinator.result_rx.borrow().clone() {
3645            None => Some(RuntimeDriverError::RuntimeStopInProgress {
3646                runtime_id: self.runtime_id.clone(),
3647            }),
3648            Some(Ok(())) => None,
3649            Some(Err(error)) => Some(error),
3650        }
3651    }
3652
3653    fn control_snapshot(&self) -> crate::driver::ephemeral::RuntimeControlProjection {
3654        self.control_projection
3655            .read()
3656            .map(|guard| guard.clone())
3657            .unwrap_or_else(|poisoned| {
3658                tracing::error!("runtime control projection lock poisoned");
3659                poisoned.into_inner().clone()
3660            })
3661    }
3662
3663    fn sync_control_projection_from_dsl_authority(&self) {
3664        let next = {
3665            let authority = self
3666                .dsl_authority
3667                .lock()
3668                .unwrap_or_else(std::sync::PoisonError::into_inner);
3669            crate::driver::ephemeral::RuntimeControlProjection {
3670                phase: crate::meerkat_machine::dsl_authority::runtime_phase_from_authority(
3671                    &authority,
3672                ),
3673                current_run_id:
3674                    crate::meerkat_machine::dsl_authority::current_run_id_from_authority(&authority),
3675                pre_run_phase: crate::meerkat_machine::dsl_authority::pre_run_phase_from_authority(
3676                    &authority,
3677                ),
3678            }
3679        };
3680        *self
3681            .control_projection
3682            .write()
3683            .unwrap_or_else(std::sync::PoisonError::into_inner) = next;
3684    }
3685
3686    fn physical_attachment_is_live(&self) -> bool {
3687        match &self.attachment_slot {
3688            RuntimeLoopAttachmentSlot::Pending(attachment)
3689            | RuntimeLoopAttachmentSlot::Attached(attachment) => {
3690                !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed()
3691            }
3692            RuntimeLoopAttachmentSlot::Empty => false,
3693        }
3694    }
3695
3696    fn attachment_is_live(&self) -> bool {
3697        matches!(
3698            &self.attachment_slot,
3699            RuntimeLoopAttachmentSlot::Attached(attachment)
3700                if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed()
3701        )
3702    }
3703
3704    fn generated_executor_registration_active(&self) -> bool {
3705        let authority = self
3706            .dsl_authority
3707            .lock()
3708            .unwrap_or_else(std::sync::PoisonError::into_inner);
3709        matches!(
3710            authority.state().registration_phase,
3711            dsl::RegistrationPhase::Active
3712        )
3713    }
3714
3715    fn generated_executor_registration_has_viable_attachment(&self) -> bool {
3716        self.generated_executor_registration_active()
3717            && match &self.attachment_slot {
3718                RuntimeLoopAttachmentSlot::Empty => true,
3719                RuntimeLoopAttachmentSlot::Pending(_) | RuntimeLoopAttachmentSlot::Attached(_) => {
3720                    self.physical_attachment_is_live()
3721                }
3722            }
3723    }
3724
3725    fn close_handle_teardown_gate(&self) {
3726        let _guard = self
3727            .dsl_authority
3728            .lock()
3729            .unwrap_or_else(std::sync::PoisonError::into_inner);
3730        self.handle_teardown_gate.close();
3731    }
3732
3733    /// True while the runtime-loop executor registration is `Active` *or*
3734    /// `Draining`. The drain window (`BeginUnregisterSession` → final
3735    /// `UnregisterSession`) keeps the session registered so the in-flight run
3736    /// can still commit and resolve its completion waiters; the runtime-loop
3737    /// driver-authority gate must therefore admit `Draining`, while the
3738    /// registration *claim* check stays `Active`-only (no new attachment may be
3739    /// granted inside the drain window).
3740    fn generated_executor_registration_active_or_draining(&self) -> bool {
3741        let authority = self
3742            .dsl_authority
3743            .lock()
3744            .unwrap_or_else(std::sync::PoisonError::into_inner);
3745        matches!(
3746            authority.state().registration_phase,
3747            dsl::RegistrationPhase::Active | dsl::RegistrationPhase::Draining
3748        )
3749    }
3750
3751    fn generated_service_turn_binding_open(&self, session_id: &SessionId) -> bool {
3752        let authority = self
3753            .dsl_authority
3754            .lock()
3755            .unwrap_or_else(std::sync::PoisonError::into_inner);
3756        let state = authority.state();
3757        self.handle_teardown_gate.is_open()
3758            && state.session_id.as_ref() == Some(&dsl::SessionId::from_domain(session_id))
3759            && state.registration_phase != dsl::RegistrationPhase::Draining
3760    }
3761
3762    fn generated_stop_deferred(&self) -> bool {
3763        self.dsl_authority
3764            .lock()
3765            .unwrap_or_else(std::sync::PoisonError::into_inner)
3766            .state()
3767            .runtime_stop_deferred
3768    }
3769
3770    fn stage_generated_executor_registration_claim(
3771        &self,
3772        session_id: &SessionId,
3773    ) -> Result<StagedSessionDslInput, String> {
3774        let staged = MeerkatMachine::stage_dsl_transition_on_authority(
3775            &self.dsl_authority,
3776            dsl::MeerkatMachineInput::EnsureSessionWithExecutor {
3777                session_id: dsl::SessionId::from_domain(session_id),
3778            },
3779            "EnsureSessionWithExecutor",
3780        )?;
3781        if self.generated_executor_registration_active() {
3782            Ok(staged)
3783        } else {
3784            let mut authority = self
3785                .dsl_authority
3786                .lock()
3787                .unwrap_or_else(std::sync::PoisonError::into_inner);
3788            authority.restore_snapshot(staged.previous_snapshot);
3789            Err("generated MeerkatMachine did not grant active executor registration".into())
3790        }
3791    }
3792
3793    fn stage_generated_executor_exit_observation(&self) -> Result<StagedSessionDslInput, String> {
3794        MeerkatMachine::stage_runtime_owner_dsl_transition_on_authority(
3795            &self.dsl_authority,
3796            crate::meerkat_machine_types::MeerkatMachineFieldlessRuntimeInternalInput::RuntimeExecutorExited,
3797        )
3798    }
3799
3800    /// Returns `true` only if the executor is fully attached with live channels.
3801    /// Used by internal publish logic within `ensure_session_with_executor`.
3802    fn has_live_attachment(&self) -> bool {
3803        self.attachment_is_live()
3804    }
3805
3806    // Attachment publishes the complete independent capability set in one
3807    // atomic slot; bundling it earlier would create a second authority shape.
3808    #[allow(clippy::too_many_arguments)]
3809    fn attach_runtime_loop(
3810        &mut self,
3811        id: RuntimeLoopAttachmentId,
3812        wake_tx: mpsc::Sender<()>,
3813        effect_tx: mpsc::Sender<crate::effect::RuntimeEffect>,
3814        serving_release: crate::runtime_loop::RuntimeLoopServingRelease,
3815        boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
3816        interrupt_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>>,
3817        publication_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorPublicationHandle>>,
3818        post_stop_cleanup_handle: Option<
3819            Arc<dyn meerkat_core::lifecycle::CoreExecutorPostStopCleanupHandle>,
3820        >,
3821        machine_managed_post_stop_unregister: bool,
3822        expected_materialization_claim: Option<&RuntimeExecutorAttachmentMaterializationClaim>,
3823        spawned_loop: crate::runtime_loop::SpawnedRuntimeLoop,
3824    ) -> Result<(), crate::runtime_loop::SpawnedRuntimeLoop> {
3825        if spawned_loop.startup_authority_transfer.is_some()
3826            || spawned_loop.serving_release.is_some()
3827        {
3828            return Err(spawned_loop);
3829        }
3830        let claim_changed = {
3831            let mut state = self
3832                .materialization_claim_state
3833                .lock()
3834                .unwrap_or_else(std::sync::PoisonError::into_inner);
3835            match expected_materialization_claim {
3836                Some(expected)
3837                    if Arc::ptr_eq(
3838                        &self.materialization_claim_state,
3839                        &expected.claim_state,
3840                    ) && state.exact_claim_is(
3841                        expected.claim_id,
3842                        &[crate::RuntimeActorMaterializationClaimPhase::ActorMaterializedPendingCommit],
3843                    ) => {}
3844                Some(_) => return Err(spawned_loop),
3845                None
3846                    if state.current.is_none()
3847                        && matches!(
3848                            state.phase,
3849                            crate::RuntimeActorMaterializationClaimPhase::Vacant
3850                                | crate::RuntimeActorMaterializationClaimPhase::RetainedActor
3851                        ) => {}
3852                None => return Err(spawned_loop),
3853            }
3854            let Some(next_legacy_generation) = state.legacy_capability_generation.checked_add(1)
3855            else {
3856                return Err(spawned_loop);
3857            };
3858            state.legacy_capability_generation = next_legacy_generation;
3859            let claim_changed = state.current.take().is_some();
3860            state.phase = crate::RuntimeActorMaterializationClaimPhase::Vacant;
3861            state.rollback_registration_available = false;
3862            claim_changed.then(|| Arc::clone(&state.changed))
3863        };
3864        if let Some(changed) = claim_changed {
3865            changed.notify_waiters();
3866        }
3867        let crate::runtime_loop::SpawnedRuntimeLoop {
3868            loop_handle,
3869            teardown_slot,
3870            startup: _,
3871            startup_authority_transfer: _,
3872            serving_release: _,
3873        } = spawned_loop;
3874        self.provisional_interrupt_handle = None;
3875        self.provisional_materialization_claim_id = None;
3876        self.runtime_stop_cleanup_coordinator = None;
3877        self.runtime_loop_teardown = Some(Arc::clone(&teardown_slot));
3878        self.publication_handle = publication_handle;
3879        self.post_stop_cleanup_handle = post_stop_cleanup_handle;
3880        self.post_stop_cleanup_attachment_id = machine_managed_post_stop_unregister.then_some(id);
3881        self.post_stop_cleanup_complete = !machine_managed_post_stop_unregister;
3882        self.post_stop_cleanup_gate = Arc::new(Mutex::new(()));
3883        self.attachment_slot = RuntimeLoopAttachmentSlot::Pending(RuntimeLoopAttachment {
3884            id,
3885            wake_tx,
3886            effect_tx,
3887            serving_release: Some(serving_release),
3888            boundary_handle,
3889            interrupt_handle,
3890            loop_handle,
3891        });
3892        Ok(())
3893    }
3894
3895    fn owns_runtime_loop_attachment(&self, expected: RuntimeLoopAttachmentId) -> bool {
3896        matches!(
3897            &self.attachment_slot,
3898            RuntimeLoopAttachmentSlot::Pending(attachment)
3899                | RuntimeLoopAttachmentSlot::Attached(attachment)
3900                if attachment.id == expected
3901        )
3902    }
3903
3904    fn live_attachment_id(&self) -> Option<RuntimeLoopAttachmentId> {
3905        match &self.attachment_slot {
3906            RuntimeLoopAttachmentSlot::Pending(attachment)
3907            | RuntimeLoopAttachmentSlot::Attached(attachment)
3908                if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
3909            {
3910                Some(attachment.id)
3911            }
3912            _ => None,
3913        }
3914    }
3915
3916    /// Detach the runtime-loop channels, returning the loop's `JoinHandle` so a
3917    /// caller can await its quiescence.
3918    ///
3919    /// Dropping the returned `wake_tx`/`effect_tx` (held inside the attachment)
3920    /// closes the loop's receivers, which drives the loop through its canonical
3921    /// `StopRuntimeExecutor` + `RuntimeExecutorExited` exit. The slot is left
3922    /// `Empty`. Returns `None` when no loop is attached.
3923    fn take_runtime_loop_attachment(&mut self) -> Option<RuntimeLoopAttachment> {
3924        match std::mem::replace(&mut self.attachment_slot, RuntimeLoopAttachmentSlot::Empty) {
3925            RuntimeLoopAttachmentSlot::Pending(attachment) => Some(attachment),
3926            RuntimeLoopAttachmentSlot::Attached(attachment) => Some(attachment),
3927            RuntimeLoopAttachmentSlot::Empty => None,
3928        }
3929    }
3930
3931    fn clear_dead_attachment(&mut self) -> bool {
3932        if matches!(
3933            self.attachment_slot,
3934            RuntimeLoopAttachmentSlot::Pending(_) | RuntimeLoopAttachmentSlot::Attached(_)
3935        ) && !self.physical_attachment_is_live()
3936        {
3937            self.attachment_slot = RuntimeLoopAttachmentSlot::Empty;
3938            return true;
3939        }
3940        false
3941    }
3942
3943    fn retire_completed_runtime_stop_after_revival(
3944        &mut self,
3945        session_id: &SessionId,
3946    ) -> Result<(), RuntimeDriverError> {
3947        if !matches!(self.attachment_slot, RuntimeLoopAttachmentSlot::Empty) {
3948            return Err(RuntimeDriverError::Internal(format!(
3949                "revived session {session_id} still carries a runtime-loop attachment"
3950            )));
3951        }
3952        match self.runtime_stop_cleanup_coordinator.as_ref() {
3953            None if self.runtime_loop_teardown.is_none() => return Ok(()),
3954            None => {
3955                return Err(RuntimeDriverError::Internal(format!(
3956                    "revived session {session_id} carries a teardown slot without its stop coordinator"
3957                )));
3958            }
3959            Some(coordinator) => {
3960                let coordinator_slot_is_current = match (
3961                    coordinator.teardown_slot.as_ref(),
3962                    self.runtime_loop_teardown.as_ref(),
3963                ) {
3964                    (Some(coordinator_slot), Some(current_slot)) => {
3965                        Arc::ptr_eq(coordinator_slot, current_slot)
3966                    }
3967                    (None, None) => true,
3968                    _ => false,
3969                };
3970                if !coordinator_slot_is_current {
3971                    return Err(RuntimeDriverError::Internal(format!(
3972                        "revived session {session_id} carries a stale runtime-stop teardown owner"
3973                    )));
3974                }
3975                match coordinator.result_rx.borrow().clone() {
3976                    Some(Ok(())) => {}
3977                    None => {
3978                        return Err(RuntimeDriverError::RuntimeStopInProgress {
3979                            runtime_id: self.runtime_id.clone(),
3980                        });
3981                    }
3982                    Some(Err(error)) => return Err(error),
3983                }
3984            }
3985        }
3986        self.runtime_stop_cleanup_coordinator = None;
3987        self.runtime_loop_teardown = None;
3988        Ok(())
3989    }
3990
3991    fn wake_sender(&self) -> Option<mpsc::Sender<()>> {
3992        match &self.attachment_slot {
3993            RuntimeLoopAttachmentSlot::Attached(attachment)
3994                if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
3995            {
3996                Some(attachment.wake_tx.clone())
3997            }
3998            _ => None,
3999        }
4000    }
4001
4002    fn effect_sender(&self) -> Option<mpsc::Sender<crate::effect::RuntimeEffect>> {
4003        match &self.attachment_slot {
4004            RuntimeLoopAttachmentSlot::Attached(attachment)
4005                if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
4006            {
4007                Some(attachment.effect_tx.clone())
4008            }
4009            _ => None,
4010        }
4011    }
4012
4013    fn boundary_handle(
4014        &self,
4015    ) -> Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>> {
4016        match &self.attachment_slot {
4017            RuntimeLoopAttachmentSlot::Attached(attachment)
4018                if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
4019            {
4020                attachment.boundary_handle.clone()
4021            }
4022            _ => None,
4023        }
4024    }
4025
4026    fn interrupt_handle(
4027        &self,
4028    ) -> Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>> {
4029        match &self.attachment_slot {
4030            RuntimeLoopAttachmentSlot::Attached(attachment)
4031                if !attachment.wake_tx.is_closed() && !attachment.effect_tx.is_closed() =>
4032            {
4033                attachment.interrupt_handle.clone()
4034            }
4035            _ => self.provisional_interrupt_handle.clone(),
4036        }
4037    }
4038
4039    fn publication_handle(
4040        &self,
4041    ) -> Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorPublicationHandle>> {
4042        self.publication_handle.clone()
4043    }
4044
4045    fn install_provisional_interrupt_handle(
4046        &mut self,
4047        claim_id: uuid::Uuid,
4048        handle: Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>,
4049    ) {
4050        if !self.physical_attachment_is_live() {
4051            self.provisional_materialization_claim_id = Some(claim_id);
4052            self.provisional_interrupt_handle = Some(handle);
4053        }
4054    }
4055
4056    fn install_provisional_post_stop_cleanup_handle(
4057        &mut self,
4058        claim_id: uuid::Uuid,
4059        handle: Arc<dyn meerkat_core::lifecycle::CoreExecutorPostStopCleanupHandle>,
4060    ) {
4061        if !self.physical_attachment_is_live() {
4062            self.provisional_materialization_claim_id = Some(claim_id);
4063            self.post_stop_cleanup_handle = Some(handle);
4064            self.post_stop_cleanup_attachment_id = Some(RuntimeLoopAttachmentId::new());
4065            self.post_stop_cleanup_complete = false;
4066            self.post_stop_cleanup_gate = Arc::new(Mutex::new(()));
4067        }
4068    }
4069}
4070
4071impl MeerkatMachine {
4072    #[cfg(test)]
4073    pub(crate) async fn model_routing_handle_for_test(
4074        &self,
4075        session_id: &SessionId,
4076    ) -> Option<Arc<crate::handles::RuntimeModelRoutingHandle>> {
4077        let (dsl_authority, visibility_owner) = {
4078            let sessions = self.sessions.read().await;
4079            let entry = sessions.get(session_id)?;
4080            (
4081                Arc::clone(&entry.dsl_authority),
4082                Arc::clone(&entry.tool_visibility_owner),
4083            )
4084        };
4085        Some(Arc::new(
4086            crate::handles::RuntimeModelRoutingHandle::new_with_visibility_owner(
4087                Arc::new(crate::handles::HandleDslAuthority::from_shared(
4088                    dsl_authority,
4089                )),
4090                visibility_owner,
4091            ),
4092        ))
4093    }
4094
4095    /// Acquire the per-session mutation gate.
4096    ///
4097    /// Returns an `Arc<Mutex<()>>` that the caller must `.lock().await` and
4098    /// hold across the full DSL-stage → driver-mutate → DSL-sync span.
4099    /// Returns `None` if the session is not registered.
4100    async fn session_mutation_gate(&self, session_id: &SessionId) -> Option<Arc<Mutex<()>>> {
4101        let sessions = self.sessions.read().await;
4102        sessions
4103            .get(session_id)
4104            .map(|entry| Arc::clone(&entry.mutation_gate))
4105    }
4106
4107    async fn lock_current_session_mutation_gate(
4108        &self,
4109        session_id: &SessionId,
4110    ) -> Option<crate::tokio::sync::OwnedMutexGuard<()>> {
4111        loop {
4112            let gate = self.session_mutation_gate(session_id).await?;
4113            let gate_guard = Arc::clone(&gate).lock_owned().await;
4114            let sessions = self.sessions.read().await;
4115            let entry = sessions.get(session_id)?;
4116            if Arc::ptr_eq(&entry.mutation_gate, &gate) {
4117                return Some(gate_guard);
4118            }
4119        }
4120    }
4121
4122    /// Deterministically pause the runtime loop after its ready-effect drain
4123    /// and before queue authority is acquired. Exposed only by test builds so
4124    /// cross-crate integration tests can admit a complete same-boundary batch.
4125    #[cfg(any(test, feature = "test-support"))]
4126    #[doc(hidden)]
4127    pub fn arm_runtime_loop_before_queue_authority_test_hook(
4128        &self,
4129        session_id: SessionId,
4130    ) -> (
4131        crate::tokio::sync::oneshot::Receiver<()>,
4132        crate::tokio::sync::oneshot::Sender<()>,
4133    ) {
4134        let (entered_tx, entered_rx) = crate::tokio::sync::oneshot::channel();
4135        let (release_tx, release_rx) = crate::tokio::sync::oneshot::channel();
4136        let mut hook = self
4137            .test_runtime_loop_before_queue_authority
4138            .lock()
4139            .unwrap_or_else(std::sync::PoisonError::into_inner);
4140        assert!(
4141            hook.is_none(),
4142            "runtime-loop queue-authority test hook already armed"
4143        );
4144        *hook = Some((session_id, entered_tx, release_rx));
4145        (entered_rx, release_tx)
4146    }
4147
4148    #[cfg(any(test, feature = "test-support"))]
4149    pub(crate) async fn run_runtime_loop_before_queue_authority_test_hook(
4150        &self,
4151        session_id: &SessionId,
4152    ) {
4153        let armed = {
4154            let mut hook = self
4155                .test_runtime_loop_before_queue_authority
4156                .lock()
4157                .unwrap_or_else(std::sync::PoisonError::into_inner);
4158            if hook
4159                .as_ref()
4160                .is_some_and(|(armed_session_id, _, _)| armed_session_id == session_id)
4161            {
4162                hook.take()
4163            } else {
4164                None
4165            }
4166        };
4167        if let Some((_, entered_tx, release_rx)) = armed {
4168            let _ = entered_tx.send(());
4169            let _ = release_rx.await;
4170        }
4171    }
4172
4173    #[cfg(test)]
4174    fn arm_control_command_after_logical_lookup_test_hook(
4175        &self,
4176        kind: ControlCommandLookupTestKind,
4177        session_id: SessionId,
4178    ) -> (
4179        crate::tokio::sync::oneshot::Receiver<()>,
4180        crate::tokio::sync::oneshot::Sender<()>,
4181    ) {
4182        let (entered_tx, entered_rx) = crate::tokio::sync::oneshot::channel();
4183        let (release_tx, release_rx) = crate::tokio::sync::oneshot::channel();
4184        let mut hook = self
4185            .test_control_command_after_logical_lookup
4186            .lock()
4187            .unwrap_or_else(std::sync::PoisonError::into_inner);
4188        assert!(
4189            hook.is_none(),
4190            "control-command lookup test hook already armed"
4191        );
4192        *hook = Some((kind, session_id, entered_tx, release_rx));
4193        (entered_rx, release_tx)
4194    }
4195
4196    #[cfg(test)]
4197    async fn run_control_command_after_logical_lookup_test_hook(
4198        &self,
4199        kind: ControlCommandLookupTestKind,
4200        session_id: &SessionId,
4201    ) {
4202        let armed = {
4203            let mut hook = self
4204                .test_control_command_after_logical_lookup
4205                .lock()
4206                .unwrap_or_else(std::sync::PoisonError::into_inner);
4207            if hook
4208                .as_ref()
4209                .is_some_and(|(armed_kind, armed_session_id, _, _)| {
4210                    *armed_kind == kind && armed_session_id == session_id
4211                })
4212            {
4213                hook.take()
4214            } else {
4215                None
4216            }
4217        };
4218        if let Some((_, _, entered_tx, release_rx)) = armed {
4219            let _ = entered_tx.send(());
4220            let _ = release_rx.await;
4221        }
4222    }
4223
4224    async fn lock_registration_gate(
4225        &self,
4226        session_id: &SessionId,
4227    ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
4228        let gate_guard = self
4229            .lock_current_session_mutation_gate(session_id)
4230            .await
4231            .ok_or(RuntimeDriverError::NotReady {
4232                state: RuntimeState::Destroyed,
4233            })?;
4234        let blocked = {
4235            let sessions = self.sessions.read().await;
4236            let entry = sessions
4237                .get(session_id)
4238                .ok_or(RuntimeDriverError::NotReady {
4239                    state: RuntimeState::Destroyed,
4240                })?;
4241            entry.registration_blocked_by_unregister(session_id)
4242        };
4243        if let Some(error) = blocked {
4244            return Err(error);
4245        }
4246        Ok(gate_guard)
4247    }
4248
4249    #[cfg(feature = "live")]
4250    async fn session_live_lifecycle_gate(&self, session_id: &SessionId) -> Option<Arc<Mutex<()>>> {
4251        let sessions = self.sessions.read().await;
4252        sessions
4253            .get(session_id)
4254            .map(|entry| Arc::clone(&entry.live_lifecycle_gate))
4255    }
4256
4257    #[cfg(feature = "live")]
4258    async fn lock_current_session_live_lifecycle_gate(
4259        &self,
4260        session_id: &SessionId,
4261    ) -> Option<(Arc<Mutex<()>>, crate::tokio::sync::OwnedMutexGuard<()>)> {
4262        loop {
4263            let gate = self.session_live_lifecycle_gate(session_id).await?;
4264            let guard = Arc::clone(&gate).lock_owned().await;
4265            let sessions = self.sessions.read().await;
4266            let entry = sessions.get(session_id)?;
4267            if Arc::ptr_eq(&entry.live_lifecycle_gate, &gate) {
4268                return Some((gate, guard));
4269            }
4270        }
4271    }
4272
4273    #[cfg(feature = "live")]
4274    async fn lock_session_mutation_gate_for_live_lifecycle_lease(
4275        &self,
4276        session_id: &SessionId,
4277        lease: &crate::member_live::MemberLiveLifecycleLease,
4278    ) -> Option<crate::tokio::sync::OwnedMutexGuard<()>> {
4279        let mutation_gate = {
4280            let sessions = self.sessions.read().await;
4281            let entry = sessions.get(session_id)?;
4282            if !lease.matches_gate(&entry.live_lifecycle_gate) {
4283                return None;
4284            }
4285            Arc::clone(&entry.mutation_gate)
4286        };
4287        let guard = Arc::clone(&mutation_gate).lock_owned().await;
4288        let sessions = self.sessions.read().await;
4289        let entry = sessions.get(session_id)?;
4290        if lease.matches_gate(&entry.live_lifecycle_gate)
4291            && Arc::ptr_eq(&entry.mutation_gate, &mutation_gate)
4292        {
4293            Some(guard)
4294        } else {
4295            None
4296        }
4297    }
4298
4299    pub(crate) async fn lock_current_session_driver_gate(
4300        &self,
4301        session_id: &SessionId,
4302        driver: &SharedDriver,
4303    ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
4304        let gate_guard = self
4305            .lock_current_session_mutation_gate(session_id)
4306            .await
4307            .ok_or(RuntimeDriverError::NotReady {
4308                state: RuntimeState::Destroyed,
4309            })?;
4310        {
4311            let sessions = self.sessions.read().await;
4312            let entry = sessions
4313                .get(session_id)
4314                .ok_or(RuntimeDriverError::NotReady {
4315                    state: RuntimeState::Destroyed,
4316                })?;
4317            if !Arc::ptr_eq(&entry.driver, driver) {
4318                return Err(RuntimeDriverError::NotReady {
4319                    state: RuntimeState::Destroyed,
4320                });
4321            }
4322        }
4323        Ok(gate_guard)
4324    }
4325
4326    pub(crate) async fn lock_current_runtime_loop_driver_authority(
4327        &self,
4328        session_id: &SessionId,
4329        driver: &SharedDriver,
4330    ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
4331        let gate_guard = self
4332            .lock_current_session_driver_gate(session_id, driver)
4333            .await?;
4334        {
4335            let sessions = self.sessions.read().await;
4336            let entry = sessions
4337                .get(session_id)
4338                .ok_or(RuntimeDriverError::NotReady {
4339                    state: RuntimeState::Destroyed,
4340                })?;
4341            if !entry.generated_executor_registration_active_or_draining() {
4342                return Err(RuntimeDriverError::ValidationFailed {
4343                    reason:
4344                        "generated MeerkatMachine has no active runtime-loop executor registration"
4345                            .to_string(),
4346                });
4347            }
4348        }
4349        Ok(gate_guard)
4350    }
4351
4352    pub(crate) async fn current_runtime_stop_cleanup_in_progress(
4353        &self,
4354        session_id: &SessionId,
4355        driver: &SharedDriver,
4356    ) -> Result<bool, RuntimeDriverError> {
4357        let sessions = self.sessions.read().await;
4358        let entry = sessions
4359            .get(session_id)
4360            .ok_or(RuntimeDriverError::NotReady {
4361                state: RuntimeState::Destroyed,
4362            })?;
4363        if !Arc::ptr_eq(&entry.driver, driver) {
4364            return Err(RuntimeDriverError::NotReady {
4365                state: RuntimeState::Destroyed,
4366            });
4367        }
4368        let Some(coordinator) = entry.runtime_stop_cleanup_coordinator.as_ref() else {
4369            return Ok(false);
4370        };
4371        if coordinator.epoch_id != entry.epoch_id {
4372            return Err(RuntimeDriverError::Internal(format!(
4373                "stale runtime-stop cleanup coordinator epoch for session {session_id}"
4374            )));
4375        }
4376        Ok(coordinator.result_rx.borrow().is_none())
4377    }
4378
4379    async fn session_dsl_authority(
4380        &self,
4381        session_id: &SessionId,
4382    ) -> Result<Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>, String> {
4383        let sessions = self.sessions.read().await;
4384        sessions
4385            .get(session_id)
4386            .map(|entry| Arc::clone(&entry.dsl_authority))
4387            .ok_or_else(|| {
4388                RuntimeDriverError::NotReady {
4389                    state: RuntimeState::Destroyed,
4390                }
4391                .to_string()
4392            })
4393    }
4394
4395    #[cfg(any(test, feature = "test-support"))]
4396    async fn session_handle_teardown_gate(
4397        &self,
4398        session_id: &SessionId,
4399    ) -> Result<Arc<crate::handles::HandleTeardownGate>, String> {
4400        let sessions = self.sessions.read().await;
4401        sessions
4402            .get(session_id)
4403            .map(|entry| Arc::clone(&entry.handle_teardown_gate))
4404            .ok_or_else(|| {
4405                RuntimeDriverError::NotReady {
4406                    state: RuntimeState::Destroyed,
4407                }
4408                .to_string()
4409            })
4410    }
4411
4412    /// Test-support: install the session's generated peer-comms handle (and its
4413    /// owner token) onto a comms runtime, so the runtime accepts generated trust
4414    /// mutations minted from THIS adapter's session dsl authority. Mirrors what
4415    /// `prepare_session_runtime_bindings` does in production via
4416    /// `SessionRuntimeBindings`, for tests/harnesses that construct external
4417    /// member runtimes directly (e.g. the external-TCP production-drain smoke
4418    /// lane). Gated behind `test-support` so it never reaches a production build.
4419    #[cfg(any(test, feature = "test-support"))]
4420    pub async fn test_install_session_peer_comms_handle_on_runtime(
4421        &self,
4422        session_id: &SessionId,
4423        runtime: &(dyn meerkat_core::handles::PeerCommsInstallTarget + '_),
4424    ) -> Result<(), String> {
4425        let dsl = self
4426            .session_dsl_authority(session_id)
4427            .await
4428            .map_err(|error| format!("session dsl authority unavailable: {error}"))?;
4429        let teardown_gate = self
4430            .session_handle_teardown_gate(session_id)
4431            .await
4432            .map_err(|error| format!("session handle teardown gate unavailable: {error}"))?;
4433        let handle = std::sync::Arc::new(
4434            crate::handles::HandleDslAuthority::from_shared_with_teardown_gate(dsl, teardown_gate),
4435        );
4436        crate::handles::RuntimePeerCommsHandle::install_generated_on(handle, runtime)
4437    }
4438
4439    fn preview_dsl_input_on_state(
4440        state: &dsl::MeerkatMachineState,
4441        input: dsl::MeerkatMachineInput,
4442        context: &str,
4443    ) -> Result<Vec<dsl::MeerkatMachineEffect>, String> {
4444        let mut preview = dsl::MeerkatMachineAuthority::recover_from_state(state.clone())
4445            .map_err(|err| dsl_authority::map_error(err, context))?;
4446        dsl::MeerkatMachineMutator::apply(&mut preview, input)
4447            .map(|transition| transition.into_effects())
4448            .map_err(|err| dsl_authority::map_error(err, context))
4449    }
4450
4451    async fn preview_session_dsl_input(
4452        &self,
4453        session_id: &SessionId,
4454        input: dsl::MeerkatMachineInput,
4455        context: &str,
4456    ) -> Result<Vec<dsl::MeerkatMachineEffect>, String> {
4457        let authority = self.session_dsl_authority(session_id).await?;
4458        let state = {
4459            let authority = authority
4460                .lock()
4461                .unwrap_or_else(std::sync::PoisonError::into_inner);
4462            authority.state().clone()
4463        };
4464        Self::preview_dsl_input_on_state(&state, input, context)
4465    }
4466
4467    async fn session_dsl_state(
4468        &self,
4469        session_id: &SessionId,
4470    ) -> Result<dsl::MeerkatMachineState, RuntimeControlPlaneError> {
4471        let authority = self
4472            .session_dsl_authority(session_id)
4473            .await
4474            .map_err(RuntimeControlPlaneError::Internal)?;
4475        let authority = authority
4476            .lock()
4477            .unwrap_or_else(std::sync::PoisonError::into_inner);
4478        Ok(authority.state().clone())
4479    }
4480
4481    async fn commit_session_dsl_transition(
4482        &self,
4483        session_id: &SessionId,
4484        staged: StagedSessionDslInput,
4485        context: &str,
4486    ) -> Result<(), String> {
4487        self.commit_session_dsl_transition_with_dispatch_failure(
4488            session_id,
4489            staged,
4490            context,
4491            CommittedEffectDispatchFailure::PreserveCommittedDslState,
4492        )
4493        .await
4494    }
4495
4496    async fn commit_session_dsl_transition_preserving_committed_state(
4497        &self,
4498        session_id: &SessionId,
4499        staged: StagedSessionDslInput,
4500        context: &str,
4501    ) -> Result<(), String> {
4502        self.commit_session_dsl_transition_with_dispatch_failure(
4503            session_id,
4504            staged,
4505            context,
4506            CommittedEffectDispatchFailure::PreserveCommittedDslState,
4507        )
4508        .await
4509    }
4510
4511    async fn commit_session_dsl_transition_with_dispatch_failure(
4512        &self,
4513        _session_id: &SessionId,
4514        staged: StagedSessionDslInput,
4515        context: &str,
4516        dispatch_failure: CommittedEffectDispatchFailure,
4517    ) -> Result<(), String> {
4518        if let Err(error) = self
4519            .dispatch_routed_signals_from_effects(&staged.effects)
4520            .await
4521        {
4522            let CommittedEffectDispatchFailure::PreserveCommittedDslState = dispatch_failure;
4523            return Err(format!(
4524                "DSL authority ({context}): committed effect dispatch failed: {error}"
4525            ));
4526        }
4527        Ok(())
4528    }
4529
4530    async fn dispatch_routed_signals_from_effects(
4531        &self,
4532        effects: &[dsl::MeerkatMachineEffect],
4533    ) -> Result<(), String> {
4534        let dispatcher = {
4535            self.composition_signal_dispatcher
4536                .read()
4537                .unwrap_or_else(std::sync::PoisonError::into_inner)
4538                .clone()
4539        };
4540        let Some(dispatcher) = dispatcher else {
4541            return Ok(());
4542        };
4543
4544        for effect in effects {
4545            if let Some(signal) = composition::lift_routed_signal(effect) {
4546                composition::dispatch_routed_signal(&dispatcher, signal).await?;
4547            }
4548        }
4549        Ok(())
4550    }
4551
4552    // The wrapper transfers one exact, non-cloneable dispatch transaction to
4553    // process-owned cleanup; a params bag would duplicate that authority shape.
4554    #[allow(clippy::too_many_arguments)]
4555    async fn dispatch_cancel_after_boundary_runtime_effect(
4556        &self,
4557        session_id: &SessionId,
4558        witness: RuntimeEffectDispatchAttachmentWitness,
4559        held_mutation_gate: crate::tokio::sync::OwnedMutexGuard<()>,
4560        boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
4561        member_authority: Option<RuntimeEffectDispatchMemberAuthority>,
4562        pending_dispatch: PendingBoundaryCancelDispatchGuard,
4563        expected_run_id: Option<&RunId>,
4564        projected_effect: crate::effect::ProjectedRuntimeEffect,
4565        dispatch_generation: u64,
4566        dispatch_lifecycle_phase: dsl::MeerkatPhase,
4567        context: &str,
4568    ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
4569        let cleanup_spawner = MachineCleanupTaskSpawner::acquire()?;
4570        let machine = self.clone();
4571        let session_id = session_id.clone();
4572        let expected_run_id = expected_run_id.cloned();
4573        let context = context.to_string();
4574        let completion = cleanup_spawner.spawn(async move {
4575            machine
4576                .dispatch_cancel_after_boundary_runtime_effect_owned(
4577                    &session_id,
4578                    witness,
4579                    held_mutation_gate,
4580                    boundary_handle,
4581                    member_authority,
4582                    pending_dispatch,
4583                    expected_run_id.as_ref(),
4584                    projected_effect,
4585                    dispatch_generation,
4586                    dispatch_lifecycle_phase,
4587                    &context,
4588                )
4589                .await
4590        });
4591        completion.await.map_err(|error| {
4592            RuntimeDriverError::Internal(format!(
4593                "process-owned boundary-effect dispatch ended without a result: {error}"
4594            ))
4595        })?
4596    }
4597
4598    // Keep every exact attachment and pending-dispatch owner visible at the
4599    // cancellation-safe process boundary rather than re-packaging authority.
4600    #[allow(clippy::too_many_arguments)]
4601    async fn dispatch_cancel_after_boundary_runtime_effect_owned(
4602        &self,
4603        session_id: &SessionId,
4604        witness: RuntimeEffectDispatchAttachmentWitness,
4605        held_mutation_gate: crate::tokio::sync::OwnedMutexGuard<()>,
4606        boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
4607        member_authority: Option<RuntimeEffectDispatchMemberAuthority>,
4608        mut pending_dispatch: PendingBoundaryCancelDispatchGuard,
4609        expected_run_id: Option<&RunId>,
4610        projected_effect: crate::effect::ProjectedRuntimeEffect,
4611        dispatch_generation: u64,
4612        dispatch_lifecycle_phase: dsl::MeerkatPhase,
4613        context: &str,
4614    ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
4615        // Executor callbacks are allowed to route back into the machine. Drop
4616        // M before invoking one, then reacquire the exact captured gate and
4617        // prove that neither the logical session nor attachment incarnation
4618        // changed before the queued effect is published.
4619        drop(held_mutation_gate);
4620        let live_dispatch_result = self
4621            .dispatch_cancel_after_boundary_live_handle(
4622                boundary_handle,
4623                expected_run_id,
4624                &projected_effect,
4625                context,
4626            )
4627            .await;
4628
4629        // Reserve bounded-channel capacity without M. A wedged executor may
4630        // delay this process-owned transaction, but it cannot retain the
4631        // session mutation authority or block teardown/replacement.
4632        let pre_reserve_state = Self::classify_dsl_boundary_cancel_dispatch(
4633            &witness.dsl_authority,
4634            dispatch_generation,
4635            &dispatch_lifecycle_phase,
4636            expected_run_id,
4637        );
4638        let effect_permit = if live_dispatch_result.is_ok()
4639            && pre_reserve_state == BoundaryCancelDispatchState::Current
4640        {
4641            Some(witness.effect_tx.clone().reserve_owned().await)
4642        } else {
4643            None
4644        };
4645
4646        let gate_guard = Arc::clone(&witness.mutation_gate).lock_owned().await;
4647        let current_effect_tx = {
4648            let sessions = self.sessions.read().await;
4649            match sessions.get(session_id) {
4650                None => Err(RuntimeDriverError::StaleAuthority {
4651                    reason: format!(
4652                        "{context}: runtime session disappeared during boundary callback"
4653                    ),
4654                }),
4655                Some(entry)
4656                    if !Arc::ptr_eq(&entry.mutation_gate, &witness.mutation_gate)
4657                        || !Arc::ptr_eq(&entry.driver, &witness.driver)
4658                        || !Arc::ptr_eq(&entry.dsl_authority, &witness.dsl_authority)
4659                        || !entry.owns_runtime_loop_attachment(witness.attachment_id) =>
4660                {
4661                    Err(RuntimeDriverError::StaleAuthority {
4662                        reason: format!(
4663                            "{context}: runtime attachment changed during boundary callback"
4664                        ),
4665                    })
4666                }
4667                Some(entry) => match entry.effect_sender() {
4668                    None => Err(RuntimeDriverError::StaleAuthority {
4669                        reason: format!(
4670                            "{context}: runtime effect channel disappeared during boundary callback"
4671                        ),
4672                    }),
4673                    Some(current_effect_tx)
4674                        if !witness.effect_tx.same_channel(&current_effect_tx) =>
4675                    {
4676                        Err(RuntimeDriverError::StaleAuthority {
4677                            reason: format!(
4678                                "{context}: runtime effect channel changed during boundary callback"
4679                            ),
4680                        })
4681                    }
4682                    Some(current_effect_tx) => Ok(current_effect_tx),
4683                },
4684            }
4685        };
4686        let current_effect_tx = current_effect_tx?;
4687
4688        if let Some(member_authority) = &member_authority {
4689            self.validate_member_effect_authority_lease_current(
4690                session_id,
4691                &member_authority.lease,
4692                member_authority.expected_member.as_ref(),
4693            )?;
4694        }
4695
4696        // Callback failure is meaningful only while the captured attachment
4697        // remains current. If replacement B won while A's callback was
4698        // outside M, the exact checks above return StaleAuthority first so A's
4699        // result cannot be mistaken for B's or trigger a caller-side retry on
4700        // B under the old request.
4701        if let Err(error) = live_dispatch_result {
4702            return Err(error);
4703        }
4704
4705        // The live turn may have crossed its boundary while M was released.
4706        // That is successful convergence; do not replay the old effect into a
4707        // successor phase. A same-generation orphan in another phase is
4708        // cleared by the generated abort input.
4709        match Self::classify_dsl_boundary_cancel_dispatch(
4710            &witness.dsl_authority,
4711            dispatch_generation,
4712            &dispatch_lifecycle_phase,
4713            expected_run_id,
4714        ) {
4715            BoundaryCancelDispatchState::ConsumedConverged => return Ok(gate_guard),
4716            BoundaryCancelDispatchState::SameGenerationOrphan
4717            | BoundaryCancelDispatchState::Superseded => {
4718                return Err(RuntimeDriverError::StaleAuthority {
4719                    reason: format!(
4720                        "{context}: boundary-cancel dispatch authority changed during callback"
4721                    ),
4722                });
4723            }
4724            BoundaryCancelDispatchState::Current => {}
4725        }
4726
4727        let state = self
4728            .existing_session_runtime_state(session_id)
4729            .await
4730            .unwrap_or(RuntimeState::Destroyed);
4731        self.reject_unregistration_drain_ingress(session_id, state)
4732            .await?;
4733
4734        debug_assert!(witness.effect_tx.same_channel(&current_effect_tx));
4735        let effect_permit = effect_permit
4736            .ok_or_else(|| RuntimeDriverError::StaleAuthority {
4737                reason: format!(
4738                    "{context}: boundary-cancel dispatch became current after capacity reservation was skipped"
4739                ),
4740            })?
4741            .map_err(|_| RuntimeDriverError::NotReady {
4742                state: RuntimeState::Idle,
4743            })?;
4744        let _effect_tx = effect_permit.send(projected_effect.into_effect());
4745        pending_dispatch.disarm();
4746        Ok(gate_guard)
4747    }
4748
4749    /// Own all post-admission boundary work for an accepted ingress request.
4750    /// Once the driver and DSL have accepted the input, caller cancellation may
4751    /// drop only the acknowledgement: cancel dispatch and wake remain
4752    /// process-owned.
4753    async fn dispatch_accepted_ingress_boundary_work(
4754        &self,
4755        session_id: &SessionId,
4756        held_mutation_gate: crate::tokio::sync::OwnedMutexGuard<()>,
4757        cancel_plan: Option<RuntimeAcceptedBoundaryCancelPlan>,
4758        _completions: crate::meerkat_machine::driver::SharedCompletionRegistry,
4759        wake_tx: Option<mpsc::Sender<()>>,
4760        should_wake: bool,
4761    ) -> Result<crate::tokio::sync::OwnedMutexGuard<()>, RuntimeDriverError> {
4762        let cleanup_spawner = MachineCleanupTaskSpawner::acquire()?;
4763        let machine = self.clone();
4764        let session_id = session_id.clone();
4765        let completion = cleanup_spawner.spawn(async move {
4766            let mut gate_guard = held_mutation_gate;
4767            if let Some(cancel_plan) = cancel_plan {
4768                gate_guard = machine
4769                    .dispatch_cancel_after_boundary_runtime_effect_owned(
4770                        &session_id,
4771                        cancel_plan.witness,
4772                        gate_guard,
4773                        cancel_plan.boundary_handle,
4774                        None,
4775                        cancel_plan.pending_dispatch,
4776                        Some(&cancel_plan.expected_run_id),
4777                        cancel_plan.projected_effect,
4778                        cancel_plan.dispatch_generation,
4779                        cancel_plan.dispatch_lifecycle_phase,
4780                        "AcceptWithCompletion",
4781                    )
4782                    .await?;
4783            }
4784            if should_wake && let Some(wake_tx) = wake_tx {
4785                let _ = wake_tx.try_send(());
4786            }
4787            Ok(gate_guard)
4788        });
4789        completion.await.map_err(|error| {
4790            RuntimeDriverError::Internal(format!(
4791                "process-owned accepted-ingress boundary transaction ended without a result: {error}"
4792            ))
4793        })?
4794    }
4795
4796    /// Invoke the executor-owned boundary hook without holding a session
4797    /// mutation mutex. Embedders may route this hook back through
4798    /// `MeerkatMachine::cancel_after_boundary`; the already-pending machine
4799    /// fact bounds that re-entry, but only if the nested call can acquire the
4800    /// gate and observe it.
4801    async fn dispatch_cancel_after_boundary_live_handle(
4802        &self,
4803        boundary_handle: Option<Arc<dyn meerkat_core::lifecycle::CoreExecutorBoundaryHandle>>,
4804        expected_run_id: Option<&RunId>,
4805        projected_effect: &crate::effect::ProjectedRuntimeEffect,
4806        context: &str,
4807    ) -> Result<(), RuntimeDriverError> {
4808        let reason = projected_effect.reason().to_string();
4809        // The cloneable live handle is exact-run authority. Attached runtimes
4810        // legitimately have no run yet; their generated CancelAfterBoundary
4811        // dispatch still goes through the in-loop executor effect below, but
4812        // must not fabricate an ID for this live callback.
4813        if let (Some(boundary_handle), Some(expected_run_id)) = (boundary_handle, expected_run_id) {
4814            boundary_handle
4815                .cancel_after_boundary(expected_run_id, reason)
4816                .await
4817                .map_err(|err| {
4818                    RuntimeDriverError::Internal(format!(
4819                        "{context}: failed to apply live boundary cancel: {err}"
4820                    ))
4821                })?;
4822        }
4823        Ok(())
4824    }
4825
4826    async fn restore_session_dsl_state(
4827        &self,
4828        session_id: &SessionId,
4829        snapshot: dsl::MeerkatMachineAuthoritySnapshot,
4830    ) {
4831        if let Ok(authority) = self.session_dsl_authority(session_id).await {
4832            Self::restore_dsl_authority_snapshot(&authority, snapshot);
4833        }
4834    }
4835
4836    async fn restore_session_dsl_state_if_current(
4837        &self,
4838        session_id: &SessionId,
4839        expected_current: dsl::MeerkatMachineAuthoritySnapshot,
4840        restore: dsl::MeerkatMachineAuthoritySnapshot,
4841    ) -> bool {
4842        let Ok(authority) = self.session_dsl_authority(session_id).await else {
4843            return false;
4844        };
4845        Self::restore_dsl_authority_snapshot_if_current(&authority, expected_current, restore)
4846    }
4847
4848    fn classify_dsl_boundary_cancel_dispatch(
4849        authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
4850        dispatch_generation: u64,
4851        dispatch_lifecycle_phase: &dsl::MeerkatPhase,
4852        expected_run_id: Option<&RunId>,
4853    ) -> BoundaryCancelDispatchState {
4854        let authority = authority
4855            .lock()
4856            .unwrap_or_else(std::sync::PoisonError::into_inner);
4857        let state = authority.state();
4858        if state.boundary_cancel_dispatch_generation != dispatch_generation {
4859            return BoundaryCancelDispatchState::Superseded;
4860        }
4861        if !state.boundary_cancel_dispatch_pending {
4862            return BoundaryCancelDispatchState::ConsumedConverged;
4863        }
4864        let expected_dsl_run_id = expected_run_id.map(dsl::RunId::from_domain);
4865        if state.lifecycle_phase == *dispatch_lifecycle_phase
4866            && state.current_run_id.as_ref() == expected_dsl_run_id.as_ref()
4867        {
4868            BoundaryCancelDispatchState::Current
4869        } else {
4870            BoundaryCancelDispatchState::SameGenerationOrphan
4871        }
4872    }
4873
4874    /// Clear only the failed boundary-dispatch fact represented by
4875    /// `dispatch_generation`. This deliberately applies a generated input to
4876    /// the current authority instead of restoring a whole pre-callback
4877    /// snapshot: the live executor hook may have re-entered the machine and
4878    /// committed unrelated state while the session mutation gate was
4879    /// released.
4880    fn abort_dsl_boundary_cancel_dispatch_if_current(
4881        authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
4882        dispatch_generation: u64,
4883    ) -> Result<bool, RuntimeDriverError> {
4884        let mut authority = authority
4885            .lock()
4886            .unwrap_or_else(std::sync::PoisonError::into_inner);
4887        if !authority.state().boundary_cancel_dispatch_pending
4888            || authority.state().boundary_cancel_dispatch_generation != dispatch_generation
4889        {
4890            return Ok(false);
4891        }
4892        dsl::MeerkatMachineMutator::apply(
4893            &mut *authority,
4894            dsl::MeerkatMachineInput::AbortCancelAfterBoundaryDispatch {
4895                dispatch_generation,
4896            },
4897        )
4898        .map(|_| true)
4899        .map_err(|err| {
4900            RuntimeDriverError::Internal(dsl_authority::map_error(
4901                err,
4902                "AbortCancelAfterBoundaryDispatch",
4903            ))
4904        })
4905    }
4906
4907    fn restore_dsl_authority_snapshot(
4908        authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
4909        snapshot: dsl::MeerkatMachineAuthoritySnapshot,
4910    ) {
4911        let mut authority = authority
4912            .lock()
4913            .unwrap_or_else(std::sync::PoisonError::into_inner);
4914        authority.restore_snapshot(snapshot);
4915    }
4916
4917    fn restore_dsl_authority_snapshot_if_current(
4918        authority: &Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
4919        expected_current: dsl::MeerkatMachineAuthoritySnapshot,
4920        restore: dsl::MeerkatMachineAuthoritySnapshot,
4921    ) -> bool {
4922        let mut authority = authority
4923            .lock()
4924            .unwrap_or_else(std::sync::PoisonError::into_inner);
4925        let current = authority.snapshot();
4926        if current.state() == expected_current.state() {
4927            authority.restore_snapshot(restore);
4928            true
4929        } else {
4930            false
4931        }
4932    }
4933}
4934
4935/// Capability token proving a session-control mutation is routed through
4936/// `MeerkatMachine` authority instead of a public store-only service path.
4937#[derive(Debug, Clone, Copy)]
4938pub struct MachineSessionControlAuthority {
4939    _private: (),
4940}
4941
4942#[cfg(feature = "live")]
4943struct LiveOpenAdmissionGeneratedAuthorityBridgeToken;
4944
4945#[cfg(feature = "live")]
4946struct LiveCloseResultGeneratedAuthorityBridgeToken;
4947
4948#[cfg(feature = "live")]
4949struct LiveChannelStatusResultGeneratedAuthorityBridgeToken;
4950
4951#[cfg(feature = "live")]
4952static LIVE_OPEN_ADMISSION_GENERATED_AUTHORITY_BRIDGE_TOKEN:
4953    LiveOpenAdmissionGeneratedAuthorityBridgeToken = LiveOpenAdmissionGeneratedAuthorityBridgeToken;
4954
4955#[cfg(feature = "live")]
4956static LIVE_CLOSE_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN:
4957    LiveCloseResultGeneratedAuthorityBridgeToken = LiveCloseResultGeneratedAuthorityBridgeToken;
4958
4959#[cfg(feature = "live")]
4960static LIVE_CHANNEL_STATUS_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN:
4961    LiveChannelStatusResultGeneratedAuthorityBridgeToken =
4962    LiveChannelStatusResultGeneratedAuthorityBridgeToken;
4963
4964#[cfg(feature = "live")]
4965fn live_open_admission_generated_authority_bridge_token()
4966-> &'static (dyn std::any::Any + Send + Sync) {
4967    &LIVE_OPEN_ADMISSION_GENERATED_AUTHORITY_BRIDGE_TOKEN
4968}
4969
4970#[cfg(feature = "live")]
4971fn live_close_result_generated_authority_bridge_token() -> &'static (dyn std::any::Any + Send + Sync)
4972{
4973    &LIVE_CLOSE_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN
4974}
4975
4976#[cfg(feature = "live")]
4977fn live_channel_status_result_generated_authority_bridge_token()
4978-> &'static (dyn std::any::Any + Send + Sync) {
4979    &LIVE_CHANNEL_STATUS_RESULT_GENERATED_AUTHORITY_BRIDGE_TOKEN
4980}
4981
4982#[cfg(feature = "live")]
4983#[doc(hidden)]
4984#[allow(improper_ctypes_definitions, unsafe_code)]
4985#[unsafe(export_name = concat!(
4986    "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_open_admission_",
4987    env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
4988))]
4989pub extern "Rust" fn live_open_admission_generated_authority_bridge_token_is_valid(
4990    token: &(dyn std::any::Any + Send + Sync),
4991) -> bool {
4992    token.is::<LiveOpenAdmissionGeneratedAuthorityBridgeToken>()
4993}
4994
4995#[cfg(feature = "live")]
4996#[doc(hidden)]
4997#[allow(improper_ctypes_definitions, unsafe_code)]
4998#[unsafe(export_name = concat!(
4999    "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_close_result_",
5000    env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
5001))]
5002pub extern "Rust" fn live_close_result_generated_authority_bridge_token_is_valid(
5003    token: &(dyn std::any::Any + Send + Sync),
5004) -> bool {
5005    token.is::<LiveCloseResultGeneratedAuthorityBridgeToken>()
5006}
5007
5008#[cfg(feature = "live")]
5009#[doc(hidden)]
5010#[allow(improper_ctypes_definitions, unsafe_code)]
5011#[unsafe(export_name = concat!(
5012    "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_live_channel_status_result_",
5013    env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
5014))]
5015pub extern "Rust" fn live_channel_status_result_generated_authority_bridge_token_is_valid(
5016    token: &(dyn std::any::Any + Send + Sync),
5017) -> bool {
5018    token.is::<LiveChannelStatusResultGeneratedAuthorityBridgeToken>()
5019}
5020
5021#[cfg(feature = "live")]
5022fn build_live_channel_open_authority(
5023    session_id: SessionId,
5024    channel_id: meerkat_live::LiveChannelId,
5025    sequence: u64,
5026) -> Result<meerkat_live::LiveChannelOpenAuthority, String> {
5027    #[allow(improper_ctypes_definitions, unsafe_code)]
5028    unsafe extern "Rust" {
5029        #[link_name = concat!(
5030            "__meerkat_live_runtime_generated_live_channel_open_authority_build_v1_",
5031            env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
5032        )]
5033        fn live_generated_channel_open_authority_build(
5034            token: &'static (dyn std::any::Any + Send + Sync),
5035            session_id: SessionId,
5036            channel_id: meerkat_live::LiveChannelId,
5037            sequence: u64,
5038        ) -> Result<meerkat_live::LiveChannelOpenAuthority, String>;
5039    }
5040    #[allow(unsafe_code)]
5041    unsafe {
5042        live_generated_channel_open_authority_build(
5043            live_open_admission_generated_authority_bridge_token(),
5044            session_id,
5045            channel_id,
5046            sequence,
5047        )
5048    }
5049}
5050
5051#[cfg(feature = "live")]
5052fn build_live_channel_close_commit_authority(
5053    channel_id: String,
5054    close_sequence: u64,
5055) -> Result<meerkat_live::LiveChannelCloseCommitAuthority, String> {
5056    #[allow(improper_ctypes_definitions, unsafe_code)]
5057    unsafe extern "Rust" {
5058        #[link_name = concat!(
5059            "__meerkat_live_runtime_generated_live_channel_close_commit_authority_build_v1_",
5060            env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
5061        )]
5062        fn live_generated_channel_close_commit_authority_build(
5063            token: &'static (dyn std::any::Any + Send + Sync),
5064            channel_id: String,
5065            close_sequence: u64,
5066        ) -> Result<meerkat_live::LiveChannelCloseCommitAuthority, String>;
5067    }
5068    #[allow(unsafe_code)]
5069    unsafe {
5070        live_generated_channel_close_commit_authority_build(
5071            live_close_result_generated_authority_bridge_token(),
5072            channel_id,
5073            close_sequence,
5074        )
5075    }
5076}
5077
5078#[cfg(feature = "live")]
5079fn build_live_channel_status_commit_authority(
5080    channel_id: String,
5081    status_observation_sequence: u64,
5082) -> Result<meerkat_live::LiveChannelStatusCommitAuthority, String> {
5083    #[allow(improper_ctypes_definitions, unsafe_code)]
5084    unsafe extern "Rust" {
5085        #[link_name = concat!(
5086            "__meerkat_live_runtime_generated_live_channel_status_commit_authority_build_v1_",
5087            env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
5088        )]
5089        fn live_generated_channel_status_commit_authority_build(
5090            token: &'static (dyn std::any::Any + Send + Sync),
5091            channel_id: String,
5092            status_observation_sequence: u64,
5093        ) -> Result<meerkat_live::LiveChannelStatusCommitAuthority, String>;
5094    }
5095    #[allow(unsafe_code)]
5096    unsafe {
5097        live_generated_channel_status_commit_authority_build(
5098            live_channel_status_result_generated_authority_bridge_token(),
5099            channel_id,
5100            status_observation_sequence,
5101        )
5102    }
5103}
5104
5105/// Generated authority output for `live/open` admission.
5106///
5107/// Constructed only from `MeerkatMachineEffect::LiveOpenAdmissionResolved`.
5108/// The live host accepts this as a typed handoff before materializing
5109/// transport resources; it does not decide duplicate-session admission from
5110/// its local maps.
5111#[derive(Debug, Clone)]
5112#[cfg(feature = "live")]
5113pub struct LiveOpenAdmissionAuthority {
5114    session_id: SessionId,
5115    channel_id: meerkat_live::LiveChannelId,
5116    admitted: bool,
5117    rejection: Option<dsl::LiveOpenAdmissionRejection>,
5118    bound_llm_identity: Option<meerkat_core::SessionLlmIdentity>,
5119    sequence: u64,
5120    channel_open_authority: Option<meerkat_live::LiveChannelOpenAuthority>,
5121}
5122
5123#[cfg(feature = "live")]
5124impl LiveOpenAdmissionAuthority {
5125    pub(crate) fn from_generated_effect(
5126        session_id: SessionId,
5127        channel_id: meerkat_live::LiveChannelId,
5128        admitted: bool,
5129        rejection: Option<dsl::LiveOpenAdmissionRejection>,
5130        bound_llm_identity: Option<dsl::SessionLlmIdentity>,
5131        sequence: u64,
5132    ) -> Result<Self, String> {
5133        let bound_llm_identity = match (admitted, bound_llm_identity) {
5134            (true, Some(identity)) => Some(identity.try_into()?),
5135            (true, None) => {
5136                return Err(
5137                    "generated live-open admission was admitted without bound LLM identity"
5138                        .to_string(),
5139                );
5140            }
5141            (false, _) => None,
5142        };
5143        let channel_open_authority = if admitted {
5144            Some(build_live_channel_open_authority(
5145                session_id.clone(),
5146                channel_id.clone(),
5147                sequence,
5148            )?)
5149        } else {
5150            None
5151        };
5152        Ok(Self {
5153            session_id,
5154            channel_id,
5155            admitted,
5156            rejection,
5157            bound_llm_identity,
5158            sequence,
5159            channel_open_authority,
5160        })
5161    }
5162
5163    #[must_use]
5164    pub fn session_id(&self) -> &SessionId {
5165        &self.session_id
5166    }
5167
5168    #[must_use]
5169    pub fn channel_id(&self) -> &meerkat_live::LiveChannelId {
5170        &self.channel_id
5171    }
5172
5173    #[must_use]
5174    pub fn admitted(&self) -> bool {
5175        self.admitted
5176    }
5177
5178    #[must_use]
5179    pub fn rejection(&self) -> Option<dsl::LiveOpenAdmissionRejection> {
5180        self.rejection
5181    }
5182
5183    #[must_use]
5184    pub fn bound_llm_identity(&self) -> Option<&meerkat_core::SessionLlmIdentity> {
5185        self.bound_llm_identity.as_ref()
5186    }
5187
5188    #[must_use]
5189    pub fn sequence(&self) -> u64 {
5190        self.sequence
5191    }
5192
5193    #[must_use]
5194    pub fn channel_open_authority(&self) -> Option<&meerkat_live::LiveChannelOpenAuthority> {
5195        self.channel_open_authority.as_ref()
5196    }
5197}
5198
5199/// Generated authority output for the public `live/refresh` success result.
5200///
5201/// Constructed only from a `MeerkatMachineEffect::LiveRefreshResultResolved`
5202/// emitted after the live adapter command queue has accepted the refresh
5203/// handoff. RPC/SDK surfaces project this value to their wire result instead
5204/// of classifying the public status from host queue mechanics.
5205#[derive(Debug, Clone, PartialEq, Eq)]
5206#[cfg(feature = "live")]
5207pub struct LiveRefreshResultAuthority {
5208    pub status: dsl::LiveRefreshPublicStatus,
5209    pub sequence: u64,
5210    pub queue_acceptance_sequence: u64,
5211}
5212
5213/// Generated authority output for the public `live/close` success result.
5214///
5215/// Constructed only from a `MeerkatMachineEffect::LiveCloseResultResolved`
5216/// emitted after the live host supplies typed close-observation evidence.
5217#[derive(Debug, Clone)]
5218#[cfg(feature = "live")]
5219pub struct LiveCloseResultAuthority {
5220    pub status: dsl::LiveClosePublicStatus,
5221    pub sequence: u64,
5222    pub close_observation_sequence: u64,
5223    channel_close_commit_authority: Option<meerkat_live::LiveChannelCloseCommitAuthority>,
5224}
5225
5226#[cfg(feature = "live")]
5227impl LiveCloseResultAuthority {
5228    pub(crate) fn from_generated_effect(
5229        channel_id: String,
5230        status: dsl::LiveClosePublicStatus,
5231        sequence: u64,
5232        close_observation_sequence: u64,
5233    ) -> Result<Self, String> {
5234        let channel_close_commit_authority = match status {
5235            dsl::LiveClosePublicStatus::Closed => Some(build_live_channel_close_commit_authority(
5236                channel_id,
5237                close_observation_sequence,
5238            )?),
5239        };
5240        Ok(Self {
5241            status,
5242            sequence,
5243            close_observation_sequence,
5244            channel_close_commit_authority,
5245        })
5246    }
5247
5248    #[must_use]
5249    pub fn channel_close_commit_authority(
5250        &self,
5251    ) -> Option<&meerkat_live::LiveChannelCloseCommitAuthority> {
5252        self.channel_close_commit_authority.as_ref()
5253    }
5254
5255    #[must_use]
5256    pub fn into_channel_close_commit_authority(
5257        self,
5258    ) -> Option<meerkat_live::LiveChannelCloseCommitAuthority> {
5259        self.channel_close_commit_authority
5260    }
5261}
5262
5263/// Generated authority output for public live command success results.
5264///
5265/// Constructed only from a `MeerkatMachineEffect::LiveCommandResultResolved`
5266/// emitted after the live host supplies typed command queue-acceptance
5267/// evidence.
5268#[derive(Debug, Clone, PartialEq, Eq)]
5269#[cfg(feature = "live")]
5270pub struct LiveCommandResultAuthority {
5271    pub command: dsl::LiveCommandPublicKind,
5272    pub sequence: u64,
5273    pub command_acceptance_sequence: u64,
5274}
5275
5276/// Generated authority output for public live command rejection results.
5277///
5278/// Constructed only from a `MeerkatMachineEffect::LiveCommandRejectionResolved`
5279/// emitted after the live host supplies typed rejection evidence. RPC/SDK
5280/// surfaces project error classes from this value instead of matching host
5281/// errors directly.
5282#[derive(Debug, Clone, PartialEq, Eq)]
5283#[cfg(feature = "live")]
5284pub struct LiveCommandRejectionAuthority {
5285    pub command: dsl::LiveCommandPublicKind,
5286    pub rejection: dsl::LiveCommandRejectionReason,
5287    pub public_error_class: dsl::LiveCommandRejectionPublicErrorClass,
5288    pub sequence: u64,
5289}
5290
5291/// Generated authority output for public live channel control request
5292/// rejections.
5293///
5294/// Constructed only from a
5295/// `MeerkatMachineEffect::LiveChannelRequestRejectionResolved` emitted after
5296/// the live host supplies typed rejection evidence.
5297#[derive(Debug, Clone, PartialEq, Eq)]
5298#[cfg(feature = "live")]
5299pub struct LiveChannelRequestRejectionAuthority {
5300    pub request: dsl::LiveChannelRequestPublicKind,
5301    pub rejection: dsl::LiveChannelRequestRejectionReason,
5302    pub public_error_class: dsl::LiveChannelRequestRejectionPublicErrorClass,
5303    pub sequence: u64,
5304}
5305
5306/// Generated authority output for a WebRTC answer token issued by
5307/// MeerkatMachine.
5308///
5309/// Constructed only from `MeerkatMachineEffect::LiveWebrtcTokenIssued`.
5310/// The transport supplies random bearer material, but it is not returned to a
5311/// caller until the generated machine records the channel binding and expiry.
5312#[derive(Debug, Clone, PartialEq, Eq)]
5313#[cfg(feature = "live")]
5314pub struct LiveWebrtcTokenAuthority {
5315    pub token: String,
5316    pub expires_at_ms: u64,
5317    pub sequence: u64,
5318}
5319
5320/// Generated authority output for WebRTC answer token admission.
5321///
5322/// Constructed only from
5323/// `MeerkatMachineEffect::LiveWebrtcAnswerAdmissionResolved`. RPC signaling
5324/// proceeds to peer setup only when this effect admits the token.
5325#[derive(Debug, Clone, PartialEq, Eq)]
5326#[cfg(feature = "live")]
5327pub struct LiveWebrtcAnswerAdmissionAuthority {
5328    pub admitted: bool,
5329    pub rejection: Option<dsl::LiveWebrtcAnswerAdmissionRejection>,
5330    pub public_error_class: Option<dsl::LiveChannelRequestRejectionPublicErrorClass>,
5331    pub sequence: u64,
5332}
5333
5334/// Generated authority output for the public `live/webrtc/answer` success
5335/// class.
5336///
5337/// Constructed only from
5338/// `MeerkatMachineEffect::LiveWebrtcAnswerResultResolved` emitted after the
5339/// WebRTC transport supplies answer-observation evidence.
5340#[derive(Debug, Clone, PartialEq, Eq)]
5341#[cfg(feature = "live")]
5342pub struct LiveWebrtcAnswerResultAuthority {
5343    pub status: dsl::LiveWebrtcAnswerPublicStatus,
5344    pub answered: bool,
5345    pub sequence: u64,
5346    pub answer_observation_sequence: u64,
5347}
5348
5349/// Generated authority output for a WebSocket transport token issued by
5350/// MeerkatMachine.
5351///
5352/// Constructed only from `MeerkatMachineEffect::LiveWebsocketTokenIssued`.
5353/// The WebSocket transport supplies random bearer material, but it is not
5354/// returned until generated authority records channel binding and expiry.
5355#[derive(Debug, Clone, PartialEq, Eq)]
5356#[cfg(feature = "live")]
5357pub struct LiveWebsocketTokenAuthority {
5358    pub token: String,
5359    pub expires_at_ms: u64,
5360    pub sequence: u64,
5361}
5362
5363/// Generated authority output for WebSocket token admission.
5364///
5365/// Constructed only from
5366/// `MeerkatMachineEffect::LiveWebsocketTokenAdmissionResolved`. The WebSocket
5367/// transport upgrade proceeds only when this effect admits the token.
5368#[derive(Debug, Clone, PartialEq, Eq)]
5369#[cfg(feature = "live")]
5370pub struct LiveWebsocketTokenAdmissionAuthority {
5371    pub admitted: bool,
5372    pub rejection: Option<dsl::LiveWebsocketTokenAdmissionRejection>,
5373    pub public_error_class: Option<dsl::LiveWebsocketTokenAdmissionPublicErrorClass>,
5374    pub sequence: u64,
5375}
5376
5377/// Generated authority output for the public `live/status` result.
5378///
5379/// Constructed only from a `MeerkatMachineEffect::LiveChannelStatusResolved`
5380/// emitted after the live host supplies typed adapter-status observation
5381/// evidence.
5382#[derive(Debug, Clone)]
5383#[cfg(feature = "live")]
5384pub struct LiveChannelStatusAuthority {
5385    pub status: dsl::LiveChannelPublicStatus,
5386    pub sequence: u64,
5387    pub status_observation_sequence: u64,
5388    pub degradation_reason: Option<dsl::LiveChannelDegradationReason>,
5389    pub degradation_detail: Option<String>,
5390    pub channel_status_commit_authority: Option<meerkat_live::LiveChannelStatusCommitAuthority>,
5391}
5392
5393#[cfg(feature = "live")]
5394impl LiveChannelStatusAuthority {
5395    pub(crate) fn from_generated_effect(
5396        channel_id: String,
5397        status: dsl::LiveChannelPublicStatus,
5398        sequence: u64,
5399        status_observation_sequence: u64,
5400        degradation_reason: Option<dsl::LiveChannelDegradationReason>,
5401        degradation_detail: Option<String>,
5402    ) -> Result<Self, String> {
5403        Ok(Self {
5404            status,
5405            sequence,
5406            status_observation_sequence,
5407            degradation_reason,
5408            degradation_detail,
5409            channel_status_commit_authority: Some(build_live_channel_status_commit_authority(
5410                channel_id,
5411                status_observation_sequence,
5412            )?),
5413        })
5414    }
5415
5416    #[must_use]
5417    pub fn channel_status_commit_authority(
5418        &self,
5419    ) -> Option<&meerkat_live::LiveChannelStatusCommitAuthority> {
5420        self.channel_status_commit_authority.as_ref()
5421    }
5422
5423    #[must_use]
5424    pub fn into_channel_status_commit_authority(
5425        self,
5426    ) -> Option<meerkat_live::LiveChannelStatusCommitAuthority> {
5427        self.channel_status_commit_authority
5428    }
5429}
5430
5431/// Session-scoped execution kernel for the Meerkat runtime.
5432///
5433/// Owns per-session runtime state (driver, ops registry, completion waiters,
5434/// comms drain, epoch bindings) and routes all internal mutations through one
5435/// canonical command reducer, with smaller group handlers retained only as
5436/// implementation detail helpers.
5437#[doc(hidden)]
5438pub struct MeerkatMachineShared {
5439    /// Per-session entries.
5440    sessions: RwLock<HashMap<SessionId, RuntimeSessionEntry>>,
5441    /// Stable process-local serialization slots for session registration and
5442    /// final unregister publication. The index retains only weak references:
5443    /// every new lookup prunes dead slots, so historical session ids cannot
5444    /// accumulate while overlapping transactions still rendezvous on one gate.
5445    registration_transaction_slots: StdRwLock<HashMap<SessionId, std::sync::Weak<Mutex<()>>>>,
5446    /// Optional RuntimeStore for persistent drivers.
5447    store: Option<Arc<dyn RuntimeStore>>,
5448    /// Blob store used by persistent drivers for durable input externalization.
5449    blob_store: Option<Arc<dyn BlobStore>>,
5450    /// Runtime-owned shell seam for live session LLM reconfiguration I/O.
5451    llm_reconfigure_host: StdRwLock<Option<Arc<dyn SessionLlmReconfigureHost>>>,
5452    /// Machine-wide injected member-observation host (multi-host mobs
5453    /// DEC-P6E-2; the `llm_reconfigure_host` precedent). Serves the
5454    /// `ReadMemberHistory` / `PollMemberEvents` drain arms and directed-turn
5455    /// admission; absent ⇒ those arms reply typed `Unavailable`.
5456    member_observation_host:
5457        StdRwLock<Option<Arc<dyn crate::member_observation::MemberObservationHost>>>,
5458    /// Machine-wide injected member live host (multi-host mobs ADJ-P6B-1;
5459    /// the `member_observation_host` precedent). Serves the four
5460    /// member-addressed live drain arms; absent ⇒ those arms reply typed
5461    /// `LiveTransportUnavailable`. Shell composition slot, NOT machine
5462    /// state — no catalog delta.
5463    member_live_host: StdRwLock<Option<Arc<dyn crate::member_live::MemberLiveHost>>>,
5464    /// Count of live bridge commands that reached this member's drain arms
5465    /// (ADJ-P6B seam S3b). Deterministic lanes pin zero-bridge-traffic
5466    /// gates against it; mechanical observability, not machine state.
5467    live_commands_served: std::sync::atomic::AtomicU64,
5468    /// Exact host-materialized incarnation registered for each resident
5469    /// member session, paired with the runtime-session mutation gate that was
5470    /// current when that residency was installed.  Remembering the gate is
5471    /// what makes a reused `SessionId` distinguishable from the runtime entry
5472    /// that the member incarnation actually materialized.
5473    /// Stable, never-replaced serialization slot for each member session id.
5474    /// Registration replacement/removal and every incarnation-fenced live
5475    /// effect acquire this same slot.  Keeping the `Arc` after unregister is
5476    /// intentional: a delayed G1 effect and a concurrent G2 install must
5477    /// rendezvous even when there was a transient unregistered interval.
5478    member_incarnation_slots: StdRwLock<HashMap<SessionId, Arc<MemberResidencySlot>>>,
5479    /// AuthMachine lifecycle authority shared by runtime-backed auth
5480    /// resolution/refresh paths and public auth-status surfaces.
5481    auth_lease: StdRwLock<meerkat_core::handles::GeneratedAuthLeaseHandle>,
5482    /// OAuth login-flow lifecycle authority shared by public auth surfaces
5483    /// that operate through this runtime adapter.
5484    #[cfg(not(target_arch = "wasm32"))]
5485    oauth_flows: StdRwLock<Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority>>,
5486    /// Runtime-scoped generated authority for live control/command rejections
5487    /// that cannot be attributed to a session because generated active-channel
5488    /// ownership has no binding for the requested channel.
5489    #[cfg(feature = "live")]
5490    live_unbound_rejection_authority: crate::driver::ephemeral::SharedIngressDslAuthority,
5491    /// Canonical owner of "this session id is currently active" — replaces
5492    /// the deleted process-global `SESSION_IDENTITY_CLAIMS` static in the
5493    /// comms shell (dogma #2). Comms runtimes acquire a typed
5494    /// [`meerkat_core::handles::SessionClaim`] through this handle and hold
5495    /// it for their lifetime; the registry is scoped to this `MeerkatMachine`
5496    /// instance, so tests / multi-runtime processes get clean isolation.
5497    session_claims: Arc<crate::handles::RuntimeSessionClaimRegistry>,
5498    /// Optional typed signal dispatcher for MeerkatMachine lifecycle
5499    /// effects routed by `meerkat_mob_seam` into MobMachine observation
5500    /// signals.
5501    composition_signal_dispatcher:
5502        StdRwLock<Option<composition::MeerkatCompositionSignalDispatcher>>,
5503    /// One-shot deterministic fault for the materializer's executor-attach
5504    /// publication window. Test-support only; production builds compile the
5505    /// post-ensure hook to a no-op and carry no field.
5506    #[cfg(feature = "test-support")]
5507    test_stop_executor_after_ensure: std::sync::atomic::AtomicBool,
5508    /// Optional deterministic pause paired with the post-ensure stop fault.
5509    /// Cross-crate tests acquire competing surface boundaries immediately
5510    /// before the exact pending attachment is stopped and cleanup begins.
5511    #[cfg(feature = "test-support")]
5512    test_pause_executor_after_ensure: std::sync::atomic::AtomicBool,
5513    #[cfg(feature = "test-support")]
5514    test_executor_after_ensure_pause_reached: crate::tokio::sync::Notify,
5515    #[cfg(feature = "test-support")]
5516    test_executor_after_ensure_pause_release: crate::tokio::sync::Notify,
5517    /// Deterministic test gate after fenced input captures its residency slot
5518    /// and exact session gate but before it locks that session gate.
5519    #[cfg(test)]
5520    test_fenced_accept_after_lease: StdMutex<
5521        Option<(
5522            crate::tokio::sync::oneshot::Sender<()>,
5523            crate::tokio::sync::oneshot::Receiver<()>,
5524        )>,
5525    >,
5526    /// One-shot positive witness for registration-slot contention. The armed
5527    /// acquisition itself uses `try_lock_owned`, reports whether it found the
5528    /// exact stable slot held, then either returns that guard or waits on that
5529    /// same slot. Test-only mechanical observation, not lifecycle authority.
5530    #[cfg(test)]
5531    test_registration_transaction_contention_probe:
5532        StdMutex<Option<(SessionId, crate::tokio::sync::oneshot::Sender<bool>)>>,
5533    /// One-shot fault after a machine-managed executor hands its exact
5534    /// post-stop mutation fence into cleanup. Proves retry reacquires instead
5535    /// of retaining or fabricating that consumed guard.
5536    #[cfg(test)]
5537    test_fail_post_stop_unregister_after_fence: StdMutex<Option<SessionId>>,
5538    /// One-shot deterministic gate after the runtime loop's first ready-effect
5539    /// drain but before it acquires queue authority. Tests publish an executor
5540    /// effect in this exact gap and prove the consumed wake is retained.
5541    #[cfg(any(test, feature = "test-support"))]
5542    test_runtime_loop_before_queue_authority: StdMutex<
5543        Option<(
5544            SessionId,
5545            crate::tokio::sync::oneshot::Sender<()>,
5546            crate::tokio::sync::oneshot::Receiver<()>,
5547        )>,
5548    >,
5549    /// One-shot deterministic gate after a logical control command resolves
5550    /// its SessionId but before it acquires the current entry's mutation gate.
5551    /// Tests replace A with B in this window to prove the command captures no
5552    /// incarnation-local handles before M.
5553    #[cfg(test)]
5554    test_control_command_after_logical_lookup: StdMutex<
5555        Option<(
5556            ControlCommandLookupTestKind,
5557            SessionId,
5558            crate::tokio::sync::oneshot::Sender<()>,
5559            crate::tokio::sync::oneshot::Receiver<()>,
5560        )>,
5561    >,
5562}
5563
5564#[cfg(test)]
5565#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5566enum ControlCommandLookupTestKind {
5567    PublishEvent,
5568    Recycle,
5569    Recover,
5570    Destroy,
5571    Retire,
5572}
5573
5574/// Exclusive lifecycle transaction for one stable member-residency slot.
5575/// Host materialization/revival acquires this before an old runtime can be
5576/// quiesced or a replacement can become reachable, then carries it back to
5577/// the host actor's durable commit point. Dropping an uncommitted update
5578/// publishes a vacant placed slot, so stale authority can never reappear
5579/// after a failed cutover.
5580pub struct MemberResidencyUpdate {
5581    adapter: Arc<MeerkatMachine>,
5582    session_id: SessionId,
5583    slot: Arc<MemberResidencySlot>,
5584    slot_guard: Option<crate::tokio::sync::OwnedMutexGuard<()>>,
5585    committed: bool,
5586}
5587
5588/// Post-commit publication guard for the normal host-delivery path. The placed
5589/// registration and journal are already visible, but no member effect may
5590/// acquire the residency slot until the host actor has published its matching
5591/// observation projection/cache. If the host responder/runtime disappears
5592/// after its durable row and machine residency commit, those durable facts are
5593/// restart authority; the process-owned publication result may be dropped as
5594/// part of that runtime-catastrophe boundary.
5595pub struct MemberResidencyPublication {
5596    _slot_guard: crate::tokio::sync::OwnedMutexGuard<()>,
5597}
5598
5599/// Reversible publication used only while an exact attachment still owns M.
5600///
5601/// The placed residency is visible synchronously to the machine commit, but
5602/// dropping this value before `finalize` restores the host-owned vacancy while
5603/// the stable residency slot is still exclusively held. This is what lets a
5604/// failed runtime-loop serving release roll back every local publication at
5605/// the same linearization point.
5606struct StagedMemberResidencyPublication {
5607    slot: Arc<MemberResidencySlot>,
5608    slot_guard: Option<crate::tokio::sync::OwnedMutexGuard<()>>,
5609}
5610
5611impl StagedMemberResidencyPublication {
5612    fn finalize(mut self) -> Result<MemberResidencyPublication, RuntimeDriverError> {
5613        let slot_guard = self.slot_guard.take().ok_or_else(|| {
5614            RuntimeDriverError::Internal(
5615                "staged member residency lost its exclusive slot guard".to_string(),
5616            )
5617        })?;
5618        Ok(MemberResidencyPublication {
5619            _slot_guard: slot_guard,
5620        })
5621    }
5622}
5623
5624impl Drop for StagedMemberResidencyPublication {
5625    fn drop(&mut self) {
5626        if self.slot_guard.is_none() {
5627            return;
5628        }
5629        *self
5630            .slot
5631            .state
5632            .write()
5633            .unwrap_or_else(std::sync::PoisonError::into_inner) =
5634            MemberResidencyState::VacantPlaced;
5635    }
5636}
5637
5638impl MemberResidencyUpdate {
5639    fn validate_for_retained_attachment(
5640        &self,
5641        adapter: &Arc<MeerkatMachine>,
5642        witness: &RuntimeExecutorAttachmentWitness,
5643        incarnation: &meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
5644    ) -> Result<(), RuntimeDriverError> {
5645        if !Arc::ptr_eq(&self.adapter, adapter) {
5646            return Err(RuntimeDriverError::StaleAuthority {
5647                reason: "member residency update belongs to another machine".to_string(),
5648            });
5649        }
5650        if &self.session_id != witness.session_id() {
5651            return Err(RuntimeDriverError::StaleAuthority {
5652                reason: format!(
5653                    "member residency update for session {} cannot publish attachment for {}",
5654                    self.session_id,
5655                    witness.session_id()
5656                ),
5657            });
5658        }
5659        if self.slot_guard.is_none() {
5660            return Err(RuntimeDriverError::Internal(
5661                "member residency update lost its publication guard".to_string(),
5662            ));
5663        }
5664        self.adapter
5665            .validate_member_incarnation_registration(&self.session_id, incarnation)
5666    }
5667
5668    fn stage_under_retained_mutation_gate(
5669        mut self,
5670        incarnation: meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
5671        tracked_turn_journal: Option<Arc<dyn crate::member_observation::TrackedTurnJournal>>,
5672        session_mutation_gate: Arc<crate::tokio::sync::Mutex<()>>,
5673    ) -> Result<StagedMemberResidencyPublication, RuntimeDriverError> {
5674        let slot_guard = self.slot_guard.take().ok_or_else(|| {
5675            RuntimeDriverError::Internal(
5676                "member residency update lost its publication guard".to_string(),
5677            )
5678        })?;
5679        *self
5680            .slot
5681            .state
5682            .write()
5683            .unwrap_or_else(std::sync::PoisonError::into_inner) =
5684            MemberResidencyState::Placed(MemberIncarnationRegistration {
5685                incarnation,
5686                session_mutation_gate,
5687                tracked_turn_journal,
5688            });
5689        self.committed = true;
5690        Ok(StagedMemberResidencyPublication {
5691            slot: Arc::clone(&self.slot),
5692            slot_guard: Some(slot_guard),
5693        })
5694    }
5695
5696    /// Publish the exact placed residency and optional tracked-turn journal
5697    /// while the lifecycle slot is still exclusively held.
5698    pub async fn commit(
5699        self,
5700        incarnation: meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
5701        tracked_turn_journal: Option<Arc<dyn crate::member_observation::TrackedTurnJournal>>,
5702    ) -> Result<MemberResidencyPublication, RuntimeDriverError> {
5703        self.adapter
5704            .validate_member_incarnation_registration(&self.session_id, &incarnation)?;
5705        let session_mutation_gate = self
5706            .adapter
5707            .lock_current_session_mutation_gate(&self.session_id)
5708            .await
5709            .ok_or(RuntimeDriverError::NotReady {
5710                state: RuntimeState::Destroyed,
5711            })?;
5712        let gate = {
5713            let sessions = self.adapter.sessions.read().await;
5714            Arc::clone(
5715                &sessions
5716                    .get(&self.session_id)
5717                    .ok_or(RuntimeDriverError::NotReady {
5718                        state: RuntimeState::Destroyed,
5719                    })?
5720                    .mutation_gate,
5721            )
5722        };
5723        let publication = self
5724            .stage_under_retained_mutation_gate(incarnation, tracked_turn_journal, gate)
5725            .and_then(StagedMemberResidencyPublication::finalize);
5726        drop(session_mutation_gate);
5727        publication
5728    }
5729
5730    /// Publish the host-owned slot as VacantPlaced while retaining the gate
5731    /// until the host actor has removed the matching observation projection.
5732    pub fn vacate(mut self) -> Result<MemberResidencyPublication, RuntimeDriverError> {
5733        let slot_guard = self.slot_guard.take().ok_or_else(|| {
5734            RuntimeDriverError::Internal(
5735                "member residency update lost its vacancy publication guard".to_string(),
5736            )
5737        })?;
5738        *self
5739            .slot
5740            .state
5741            .write()
5742            .unwrap_or_else(std::sync::PoisonError::into_inner) =
5743            MemberResidencyState::VacantPlaced;
5744        self.committed = true;
5745        Ok(MemberResidencyPublication {
5746            _slot_guard: slot_guard,
5747        })
5748    }
5749}
5750
5751impl Drop for MemberResidencyUpdate {
5752    fn drop(&mut self) {
5753        if self.committed {
5754            return;
5755        }
5756        *self
5757            .slot
5758            .state
5759            .write()
5760            .unwrap_or_else(std::sync::PoisonError::into_inner) =
5761            MemberResidencyState::VacantPlaced;
5762    }
5763}
5764
5765/// Cloneable handle to the process-local runtime authority.
5766///
5767/// Clones share every machine-owned session fact and shell-mechanics lock.
5768/// The owned unregister coordinator uses a clone so dropping any individual
5769/// stop/unregister caller can never cancel teardown halfway through the
5770/// generated `Draining` window.
5771#[derive(Clone)]
5772pub struct MeerkatMachine {
5773    shared: Arc<MeerkatMachineShared>,
5774}
5775
5776impl std::ops::Deref for MeerkatMachine {
5777    type Target = MeerkatMachineShared;
5778
5779    fn deref(&self) -> &Self::Target {
5780        self.shared.as_ref()
5781    }
5782}
5783
5784impl MeerkatMachine {
5785    /// Return the stable transaction slot for one session id.
5786    ///
5787    /// Cold registration holds this slot across durable recovery/ops-epoch
5788    /// initialization and session-map publication. Unregister uses one section
5789    /// to install its exact coordinator and another around durable finalization
5790    /// plus compare-and-remove; the present coordinator/Draining entry fences
5791    /// the quiescence interval between them. A weak index preserves rendezvous
5792    /// for overlapping operations without retaining every session id for the
5793    /// lifetime of the machine.
5794    fn session_registration_transaction_slot(&self, session_id: &SessionId) -> Arc<Mutex<()>> {
5795        let mut slots = self
5796            .registration_transaction_slots
5797            .write()
5798            .unwrap_or_else(std::sync::PoisonError::into_inner);
5799        if let Some(slot) = slots.get(session_id).and_then(std::sync::Weak::upgrade) {
5800            return slot;
5801        }
5802        slots.retain(|_, slot| slot.upgrade().is_some());
5803        let slot = Arc::new(Mutex::new(()));
5804        slots.insert(session_id.clone(), Arc::downgrade(&slot));
5805        slot
5806    }
5807
5808    async fn lock_session_registration_transaction(
5809        &self,
5810        session_id: &SessionId,
5811    ) -> crate::tokio::sync::OwnedMutexGuard<()> {
5812        let slot = self.session_registration_transaction_slot(session_id);
5813        #[cfg(test)]
5814        let contention_probe = {
5815            let mut probe = self
5816                .test_registration_transaction_contention_probe
5817                .lock()
5818                .unwrap_or_else(std::sync::PoisonError::into_inner);
5819            if probe
5820                .as_ref()
5821                .is_some_and(|(probe_session_id, _)| probe_session_id == session_id)
5822            {
5823                probe.take().map(|(_, sender)| sender)
5824            } else {
5825                None
5826            }
5827        };
5828        #[cfg(test)]
5829        if let Some(contention_probe) = contention_probe {
5830            return match Arc::clone(&slot).try_lock_owned() {
5831                Ok(guard) => {
5832                    let _ = contention_probe.send(false);
5833                    guard
5834                }
5835                Err(_) => {
5836                    let _ = contention_probe.send(true);
5837                    slot.lock_owned().await
5838                }
5839            };
5840        }
5841        slot.lock_owned().await
5842    }
5843
5844    #[cfg(test)]
5845    pub(crate) fn probe_next_session_registration_transaction_contention_for_test(
5846        &self,
5847        session_id: &SessionId,
5848    ) -> crate::tokio::sync::oneshot::Receiver<bool> {
5849        let (sender, receiver) = crate::tokio::sync::oneshot::channel();
5850        *self
5851            .test_registration_transaction_contention_probe
5852            .lock()
5853            .unwrap_or_else(std::sync::PoisonError::into_inner) =
5854            Some((session_id.clone(), sender));
5855        receiver
5856    }
5857
5858    /// Begin an authority-owned placed-residency lifecycle update. The stable
5859    /// slot Arc is never removed, even when this transaction aborts.
5860    pub async fn begin_member_residency_update(
5861        self: &Arc<Self>,
5862        session_id: SessionId,
5863    ) -> MemberResidencyUpdate {
5864        let slot = self.member_incarnation_slot(&session_id);
5865        let slot_guard = Arc::clone(&slot.gate).lock_owned().await;
5866        *slot
5867            .state
5868            .write()
5869            .unwrap_or_else(std::sync::PoisonError::into_inner) =
5870            MemberResidencyState::VacantPlaced;
5871        MemberResidencyUpdate {
5872            adapter: Arc::clone(self),
5873            session_id,
5874            slot,
5875            slot_guard: Some(slot_guard),
5876            committed: false,
5877        }
5878    }
5879
5880    /// Capability token for store-only session-control mutations routed
5881    /// through this machine authority.
5882    #[must_use]
5883    pub fn session_control_authority(&self) -> MachineSessionControlAuthority {
5884        MachineSessionControlAuthority { _private: () }
5885    }
5886
5887    /// Install the machine-wide member-observation host (multi-host mobs
5888    /// DEC-P6E-2). Called by the composing surface (the mob host daemon);
5889    /// sessions resolve per call, so residency establishment needs no
5890    /// per-session install.
5891    pub fn set_member_observation_host(
5892        &self,
5893        host: Arc<dyn crate::member_observation::MemberObservationHost>,
5894    ) {
5895        *self
5896            .member_observation_host
5897            .write()
5898            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(host);
5899    }
5900
5901    /// Resolve the injected member-observation host, if composed.
5902    pub(crate) fn member_observation_host(
5903        &self,
5904    ) -> Option<Arc<dyn crate::member_observation::MemberObservationHost>> {
5905        self.member_observation_host
5906            .read()
5907            .unwrap_or_else(std::sync::PoisonError::into_inner)
5908            .clone()
5909    }
5910
5911    /// Install the machine-wide member live host (multi-host mobs
5912    /// ADJ-P6B-1). Called by the composing surface (the mob host daemon,
5913    /// live-capable `rkat-rpc`); sessions resolve per call, so member
5914    /// materialization needs no per-session install.
5915    pub fn set_member_live_host(&self, host: Arc<dyn crate::member_live::MemberLiveHost>) {
5916        *self
5917            .member_live_host
5918            .write()
5919            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(host);
5920    }
5921
5922    /// Resolve the injected member live host, if composed.
5923    pub(crate) fn member_live_host(&self) -> Option<Arc<dyn crate::member_live::MemberLiveHost>> {
5924        self.member_live_host
5925            .read()
5926            .unwrap_or_else(std::sync::PoisonError::into_inner)
5927            .clone()
5928    }
5929
5930    /// Acquire the session's live/lifecycle lease for a complete `live/open`
5931    /// materialization.  Every surface (RPC and mob member gateways) enters
5932    /// the same orchestration path and therefore the same machine-owned gate.
5933    #[cfg(feature = "live")]
5934    pub async fn acquire_live_open_lifecycle_lease(
5935        &self,
5936        session_id: &meerkat_core::types::SessionId,
5937    ) -> Result<crate::member_live::MemberLiveLifecycleLease, RuntimeDriverError> {
5938        let (gate, guard) = self
5939            .lock_current_session_live_lifecycle_gate(session_id)
5940            .await
5941            .ok_or(RuntimeDriverError::NotReady {
5942                state: RuntimeState::Destroyed,
5943            })?;
5944        Ok(crate::member_live::MemberLiveLifecycleLease::new(
5945            session_id.clone(),
5946            gate,
5947            guard,
5948        ))
5949    }
5950
5951    /// Acquire the same lease as `live/open`, prove physical live-channel
5952    /// absence, and return it still held.  The lifecycle owner must retain the
5953    /// returned value through its durable retire/archive marker; dropping it
5954    /// earlier would reopen the original RPC-open race.
5955    #[cfg(feature = "live")]
5956    pub async fn acquire_member_live_disposal_lease(
5957        &self,
5958        session_id: &meerkat_core::types::SessionId,
5959    ) -> Result<crate::member_live::MemberLiveLifecycleLease, crate::member_live::MemberLiveError>
5960    {
5961        let (gate, guard) = self
5962            .lock_current_session_live_lifecycle_gate(session_id)
5963            .await
5964            .ok_or_else(|| crate::member_live::MemberLiveError::Unavailable {
5965                reason: format!(
5966                    "member-live lifecycle gate is unavailable for session {session_id}"
5967                ),
5968            })?;
5969        let lease =
5970            crate::member_live::MemberLiveLifecycleLease::new(session_id.clone(), gate, guard);
5971
5972        self.prove_member_live_absence_while_lease_held(session_id, &lease)
5973            .await?;
5974        Ok(lease)
5975    }
5976
5977    /// Close any active channel while the caller retains this exact session's
5978    /// live/lifecycle lease. Exact construction rollback uses this split form:
5979    /// raw lease first, mutation gate second, identity revalidation third,
5980    /// physical absence proof last. That order cannot close a same-ID
5981    /// replacement before discovering that its epoch/claim changed.
5982    #[cfg(feature = "live")]
5983    async fn prove_member_live_absence_while_lease_held(
5984        &self,
5985        session_id: &meerkat_core::types::SessionId,
5986        lease: &crate::member_live::MemberLiveLifecycleLease,
5987    ) -> Result<(), crate::member_live::MemberLiveError> {
5988        if lease.session_id() != session_id {
5989            return Err(crate::member_live::MemberLiveError::Internal {
5990                reason: format!(
5991                    "member-live lifecycle lease for session {} cannot dispose session {session_id}",
5992                    lease.session_id()
5993                ),
5994            });
5995        }
5996        let lease_is_current = {
5997            let sessions = self.sessions.read().await;
5998            sessions
5999                .get(session_id)
6000                .is_some_and(|entry| lease.matches_gate(&entry.live_lifecycle_gate))
6001        };
6002        if !lease_is_current {
6003            return Err(crate::member_live::MemberLiveError::Unavailable {
6004                reason: format!("member-live lifecycle lease is stale for session {session_id}"),
6005            });
6006        }
6007
6008        let Some(channel_id) = self.live_active_channel_for_session(session_id).await else {
6009            return Ok(());
6010        };
6011        let Some(host) = self.member_live_host() else {
6012            return Err(crate::member_live::MemberLiveError::Unavailable {
6013                reason: format!(
6014                    "session {session_id} still owns live channel '{channel_id}', but no transport-neutral live cleanup host is composed"
6015                ),
6016            });
6017        };
6018        let timeout = crate::member_live::MEMBER_LIVE_DISPOSAL_CEILING;
6019        match crate::tokio::time::timeout(timeout, host.close(session_id, channel_id.as_str()))
6020            .await
6021        {
6022            Ok(Ok(_)) => {}
6023            Ok(Err(error)) => return Err(error),
6024            Err(_) => {
6025                return Err(crate::member_live::MemberLiveError::Unavailable {
6026                    reason: format!(
6027                        "member-live disposal close for channel '{channel_id}' timed out after {}ms for session {session_id}",
6028                        timeout.as_millis()
6029                    ),
6030                });
6031            }
6032        }
6033        if let Some(still_active) = self.live_active_channel_for_session(session_id).await {
6034            return Err(crate::member_live::MemberLiveError::Internal {
6035                reason: format!(
6036                    "member-live close returned success but machine channel '{still_active}' remains active for session {session_id}"
6037                ),
6038            });
6039        }
6040        Ok(())
6041    }
6042
6043    /// Prove this session has no active member-live channel before an owning
6044    /// lifecycle tears down its runtime/session binding. The machine owns the
6045    /// injected host slot, so disposal does not receive or cache a second live
6046    /// gateway. A missing host is accepted only when generated authority has
6047    /// no active channel; active generated ownership without a cleanup host is
6048    /// a composition error, never negative transport evidence.
6049    ///
6050    /// Each host call is bounded independently. Any timeout or ambiguous host
6051    /// failure is returned to the caller, which must keep its lifecycle marker
6052    /// uncommitted and retry. Machine-owned absence or a successful exact close
6053    /// proves absence; an unexpected host `ChannelNotFound` remains a custody
6054    /// mismatch rather than being laundered into success.
6055    #[cfg(feature = "live")]
6056    pub async fn close_member_live_channel_for_disposal(
6057        &self,
6058        session_id: &meerkat_core::types::SessionId,
6059    ) -> Result<(), crate::member_live::MemberLiveError> {
6060        let _lease = self.acquire_member_live_disposal_lease(session_id).await?;
6061        Ok(())
6062    }
6063
6064    /// Record one live bridge command arriving at this member's drain
6065    /// (counted at host resolution, before any reach — absent-slot rejects
6066    /// count too: the command DID reach the member).
6067    pub(crate) fn note_live_command_served(&self) {
6068        self.live_commands_served
6069            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6070    }
6071
6072    /// Live bridge commands served by this member's drain arms (S3b).
6073    pub fn live_commands_served(&self) -> u64 {
6074        self.live_commands_served
6075            .load(std::sync::atomic::Ordering::Relaxed)
6076    }
6077
6078    /// Resolve a session's tracked-turn journal, if registered. Public for
6079    /// member-host restart recovery: durable Pending rows must reattach the
6080    /// exact residency's journal without re-executing their input.
6081    pub fn tracked_turn_journal(
6082        &self,
6083        session_id: &SessionId,
6084    ) -> Option<Arc<dyn crate::member_observation::TrackedTurnJournal>> {
6085        let slot = self
6086            .member_incarnation_slots
6087            .read()
6088            .unwrap_or_else(std::sync::PoisonError::into_inner)
6089            .get(session_id)
6090            .cloned()?;
6091        let state = slot
6092            .state
6093            .read()
6094            .unwrap_or_else(std::sync::PoisonError::into_inner);
6095        match &*state {
6096            MemberResidencyState::Placed(registration) => registration.tracked_turn_journal.clone(),
6097            MemberResidencyState::PeerOnly | MemberResidencyState::VacantPlaced => None,
6098        }
6099    }
6100
6101    fn validate_member_incarnation_registration(
6102        &self,
6103        session_id: &SessionId,
6104        incarnation: &meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
6105    ) -> Result<(), RuntimeDriverError> {
6106        if incarnation.mob_id.is_empty()
6107            || incarnation.agent_identity.is_empty()
6108            || incarnation.host_id.is_empty()
6109            || incarnation.member_session_id != session_id.to_string()
6110            || incarnation.binding_generation == 0
6111            || incarnation.fence_token == 0
6112        {
6113            return Err(RuntimeDriverError::ValidationFailed {
6114                reason: format!(
6115                    "member incarnation registration requires complete identities plus nonzero host-binding generation and fence for session '{session_id}': {incarnation:?}"
6116                ),
6117            });
6118        }
6119        Ok(())
6120    }
6121
6122    /// Resolve the exact host-materialized incarnation for a resident
6123    /// session. `None` means either true PeerOnly or VacantPlaced; callers
6124    /// that need to authorize peer-only effects must use the residency lease,
6125    /// which distinguishes those states.
6126    pub fn member_incarnation(
6127        &self,
6128        session_id: &SessionId,
6129    ) -> Option<meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation> {
6130        let slot = self
6131            .member_incarnation_slots
6132            .read()
6133            .unwrap_or_else(std::sync::PoisonError::into_inner)
6134            .get(session_id)
6135            .cloned()?;
6136        let state = slot
6137            .state
6138            .read()
6139            .unwrap_or_else(std::sync::PoisonError::into_inner);
6140        match &*state {
6141            MemberResidencyState::Placed(registration) => Some(registration.incarnation.clone()),
6142            MemberResidencyState::PeerOnly | MemberResidencyState::VacantPlaced => None,
6143        }
6144    }
6145
6146    #[cfg(test)]
6147    pub(crate) fn test_member_residency_is_vacant(&self, session_id: &SessionId) -> bool {
6148        self.member_incarnation_slots
6149            .read()
6150            .unwrap_or_else(std::sync::PoisonError::into_inner)
6151            .get(session_id)
6152            .is_some_and(|slot| {
6153                matches!(
6154                    &*slot
6155                        .state
6156                        .read()
6157                        .unwrap_or_else(std::sync::PoisonError::into_inner),
6158                    MemberResidencyState::VacantPlaced
6159                )
6160            })
6161    }
6162
6163    fn member_incarnation_slot(&self, session_id: &SessionId) -> Arc<MemberResidencySlot> {
6164        if let Some(slot) = self
6165            .member_incarnation_slots
6166            .read()
6167            .unwrap_or_else(std::sync::PoisonError::into_inner)
6168            .get(session_id)
6169            .cloned()
6170        {
6171            return slot;
6172        }
6173        Arc::clone(
6174            self.member_incarnation_slots
6175                .write()
6176                .unwrap_or_else(std::sync::PoisonError::into_inner)
6177                .entry(session_id.clone())
6178                .or_insert_with(|| Arc::new(MemberResidencySlot::peer_only())),
6179        )
6180    }
6181
6182    /// Acquire the stable member-residency slot and the exact runtime-session
6183    /// gate installed with `expected`.  Holding both guards through the live
6184    /// effect makes validation and mutation one atomic authority interval:
6185    /// neither a G2 incarnation install nor a reused-session registration can
6186    /// overtake a G1 effect after its comparison.
6187    pub(crate) async fn lock_member_effect_authority(
6188        &self,
6189        session_id: &SessionId,
6190        expected: &meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
6191    ) -> Result<MemberEffectAuthorityGuard, RuntimeDriverError> {
6192        self.lock_optional_member_effect_authority(session_id, Some(expected))
6193            .await
6194    }
6195
6196    /// The peer-only sibling of [`Self::lock_member_effect_authority`]. A
6197    /// `None` expectation is still an authority claim: it holds the stable
6198    /// slot while proving that no host residency is registered, so a placed
6199    /// incarnation cannot appear between classification and effect.
6200    pub(crate) async fn lock_optional_member_effect_authority(
6201        &self,
6202        session_id: &SessionId,
6203        expected: Option<&meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation>,
6204    ) -> Result<MemberEffectAuthorityGuard, RuntimeDriverError> {
6205        let lease = self
6206            .acquire_member_effect_authority_lease(session_id, expected)
6207            .await?;
6208        let MemberEffectAuthorityLease {
6209            slot_guard,
6210            session_mutation_gate,
6211            ..
6212        } = lease;
6213        let session_guard = Arc::clone(&session_mutation_gate).lock_owned().await;
6214        {
6215            let sessions = self.sessions.read().await;
6216            let Some(entry) = sessions.get(session_id) else {
6217                return Err(RuntimeDriverError::StaleAuthority {
6218                    reason: format!(
6219                        "member effect expected incarnation {expected:?}; runtime session is absent"
6220                    ),
6221                });
6222            };
6223            if !Arc::ptr_eq(&entry.mutation_gate, &session_mutation_gate) {
6224                return Err(RuntimeDriverError::StaleAuthority {
6225                    reason: format!(
6226                        "member effect expected incarnation {expected:?}; runtime session was replaced"
6227                    ),
6228                });
6229            }
6230        }
6231        let state = self
6232            .existing_session_runtime_state(session_id)
6233            .await
6234            .unwrap_or(RuntimeState::Destroyed);
6235        self.reject_unregistration_drain_ingress(session_id, state)
6236            .await?;
6237        Ok(MemberEffectAuthorityGuard {
6238            _slot_guard: slot_guard,
6239            _session_guard: session_guard,
6240        })
6241    }
6242
6243    async fn acquire_member_effect_authority_lease(
6244        &self,
6245        session_id: &SessionId,
6246        expected: Option<&meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation>,
6247    ) -> Result<MemberEffectAuthorityLease, RuntimeDriverError> {
6248        let slot = self.member_incarnation_slot(session_id);
6249        let slot_guard = Arc::clone(&slot.gate).lock_owned().await;
6250        let registered_gate = {
6251            let state = slot
6252                .state
6253                .read()
6254                .unwrap_or_else(std::sync::PoisonError::into_inner);
6255            match (expected, &*state) {
6256                (Some(expected), MemberResidencyState::Placed(registration))
6257                    if &registration.incarnation == expected =>
6258                {
6259                    Some(Arc::clone(&registration.session_mutation_gate))
6260                }
6261                (Some(expected), MemberResidencyState::Placed(registration)) => {
6262                    return Err(RuntimeDriverError::StaleAuthority {
6263                        reason: format!(
6264                            "member effect expected incarnation {expected:?}; current is {:?}",
6265                            registration.incarnation
6266                        ),
6267                    });
6268                }
6269                (
6270                    Some(expected),
6271                    MemberResidencyState::PeerOnly | MemberResidencyState::VacantPlaced,
6272                ) => {
6273                    return Err(RuntimeDriverError::StaleAuthority {
6274                        reason: format!(
6275                            "member effect expected incarnation {expected:?}; current residency is absent"
6276                        ),
6277                    });
6278                }
6279                (None, MemberResidencyState::Placed(registration)) => {
6280                    return Err(RuntimeDriverError::StaleAuthority {
6281                        reason: format!(
6282                            "peer-only member effect expected no host residency; current is {:?}",
6283                            registration.incarnation
6284                        ),
6285                    });
6286                }
6287                (None, MemberResidencyState::VacantPlaced) => {
6288                    return Err(RuntimeDriverError::StaleAuthority {
6289                        reason: "peer-only member effect cannot address a vacant placed residency"
6290                            .to_string(),
6291                    });
6292                }
6293                (None, MemberResidencyState::PeerOnly) => None,
6294            }
6295        };
6296        let registered_gate = match registered_gate {
6297            Some(gate) => gate,
6298            None => self.session_mutation_gate(session_id).await.ok_or(
6299                RuntimeDriverError::NotReady {
6300                    state: RuntimeState::Destroyed,
6301                },
6302            )?,
6303        };
6304        {
6305            let sessions = self.sessions.read().await;
6306            let Some(entry) = sessions.get(session_id) else {
6307                return Err(RuntimeDriverError::StaleAuthority {
6308                    reason: format!(
6309                        "member effect expected incarnation {expected:?}; runtime session is absent"
6310                    ),
6311                });
6312            };
6313            if !Arc::ptr_eq(&entry.mutation_gate, &registered_gate) {
6314                return Err(RuntimeDriverError::StaleAuthority {
6315                    reason: format!(
6316                        "member effect expected incarnation {expected:?}; runtime session was replaced"
6317                    ),
6318                });
6319            }
6320        }
6321        Ok(MemberEffectAuthorityLease {
6322            slot,
6323            slot_guard,
6324            session_mutation_gate: registered_gate,
6325        })
6326    }
6327
6328    /// Revalidate the residency half of a retained member-effect lease without
6329    /// reacquiring its slot gate. The caller still owns `lease.slot_guard`, so
6330    /// this read is both race-free and preserves the slot -> session lock order.
6331    fn validate_member_effect_authority_lease_current(
6332        &self,
6333        session_id: &SessionId,
6334        lease: &MemberEffectAuthorityLease,
6335        expected: Option<&meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation>,
6336    ) -> Result<(), RuntimeDriverError> {
6337        let slot_is_current = self
6338            .member_incarnation_slots
6339            .read()
6340            .unwrap_or_else(std::sync::PoisonError::into_inner)
6341            .get(session_id)
6342            .is_some_and(|slot| Arc::ptr_eq(slot, &lease.slot));
6343        if !slot_is_current {
6344            return Err(RuntimeDriverError::StaleAuthority {
6345                reason: "member effect residency slot was replaced".to_string(),
6346            });
6347        }
6348
6349        let state = lease
6350            .slot
6351            .state
6352            .read()
6353            .unwrap_or_else(std::sync::PoisonError::into_inner);
6354        match (expected, &*state) {
6355            (Some(expected), MemberResidencyState::Placed(registration))
6356                if &registration.incarnation == expected
6357                    && Arc::ptr_eq(
6358                        &registration.session_mutation_gate,
6359                        &lease.session_mutation_gate,
6360                    ) =>
6361            {
6362                Ok(())
6363            }
6364            (Some(expected), MemberResidencyState::Placed(registration)) => {
6365                Err(RuntimeDriverError::StaleAuthority {
6366                    reason: format!(
6367                        "member effect expected incarnation {expected:?}; current is {:?}",
6368                        registration.incarnation
6369                    ),
6370                })
6371            }
6372            (
6373                Some(expected),
6374                MemberResidencyState::PeerOnly | MemberResidencyState::VacantPlaced,
6375            ) => Err(RuntimeDriverError::StaleAuthority {
6376                reason: format!(
6377                    "member effect expected incarnation {expected:?}; current residency is absent"
6378                ),
6379            }),
6380            (None, MemberResidencyState::PeerOnly) => Ok(()),
6381            (None, MemberResidencyState::Placed(registration)) => {
6382                Err(RuntimeDriverError::StaleAuthority {
6383                    reason: format!(
6384                        "peer-only member effect expected no host residency; current is {:?}",
6385                        registration.incarnation
6386                    ),
6387                })
6388            }
6389            (None, MemberResidencyState::VacantPlaced) => Err(RuntimeDriverError::StaleAuthority {
6390                reason: "peer-only member effect cannot address a vacant placed residency"
6391                    .to_string(),
6392            }),
6393        }
6394    }
6395
6396    /// Whether this adapter shares the same runtime persistence authority as
6397    /// another adapter. Runtime-backed composition surfaces use this to reject
6398    /// mismatched adapters before visible terminal events can outrun the store
6399    /// that owns their durable commit.
6400    #[must_use]
6401    pub fn shares_runtime_persistence_with(&self, other: &Self) -> bool {
6402        match (&self.store, &other.store) {
6403            (None, None) => true,
6404            (Some(a), Some(b)) => runtime_stores_share_authority(a, b),
6405            _ => false,
6406        }
6407    }
6408
6409    /// Whether this adapter owns the same runtime persistence authority as a
6410    /// concrete runtime store handle.
6411    #[must_use]
6412    pub fn shares_runtime_store_authority(&self, store: &Arc<dyn RuntimeStore>) -> bool {
6413        self.store
6414            .as_ref()
6415            .is_some_and(|machine_store| runtime_stores_share_authority(machine_store, store))
6416    }
6417
6418    /// Whether this adapter has a runtime persistence store.
6419    #[must_use]
6420    pub fn has_runtime_persistence(&self) -> bool {
6421        self.store.is_some()
6422    }
6423
6424    fn normalize_destroyed_error(err: RuntimeDriverError) -> RuntimeDriverError {
6425        match err {
6426            RuntimeDriverError::NotReady {
6427                state: RuntimeState::Destroyed,
6428            } => RuntimeDriverError::Destroyed,
6429            other => other,
6430        }
6431    }
6432
6433    /// Create an ephemeral adapter (all sessions use EphemeralRuntimeDriver).
6434    pub fn ephemeral() -> Self {
6435        let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
6436        #[cfg(not(target_arch = "wasm32"))]
6437        let oauth_flows = Arc::new(crate::handles::RuntimeOAuthFlowHandle::new_with_auth_lease(
6438            std::time::Duration::from_secs(10 * 60),
6439            Arc::clone(&auth_lease),
6440        ));
6441        let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
6442        Self {
6443            shared: Arc::new(MeerkatMachineShared {
6444                sessions: RwLock::new(HashMap::new()),
6445                registration_transaction_slots: StdRwLock::new(HashMap::new()),
6446                store: None,
6447                blob_store: None,
6448                llm_reconfigure_host: StdRwLock::new(None),
6449                member_observation_host: StdRwLock::new(None),
6450                member_live_host: StdRwLock::new(None),
6451                live_commands_served: std::sync::atomic::AtomicU64::new(0),
6452                member_incarnation_slots: StdRwLock::new(HashMap::new()),
6453                auth_lease: StdRwLock::new(auth_lease),
6454                #[cfg(not(target_arch = "wasm32"))]
6455                oauth_flows: StdRwLock::new(oauth_flows),
6456                #[cfg(feature = "live")]
6457                live_unbound_rejection_authority: live_unbound_rejection_authority(),
6458                session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
6459                composition_signal_dispatcher: StdRwLock::new(None),
6460                #[cfg(feature = "test-support")]
6461                test_stop_executor_after_ensure: std::sync::atomic::AtomicBool::new(false),
6462                #[cfg(feature = "test-support")]
6463                test_pause_executor_after_ensure: std::sync::atomic::AtomicBool::new(false),
6464                #[cfg(feature = "test-support")]
6465                test_executor_after_ensure_pause_reached: crate::tokio::sync::Notify::new(),
6466                #[cfg(feature = "test-support")]
6467                test_executor_after_ensure_pause_release: crate::tokio::sync::Notify::new(),
6468                #[cfg(test)]
6469                test_fenced_accept_after_lease: StdMutex::new(None),
6470                #[cfg(test)]
6471                test_registration_transaction_contention_probe: StdMutex::new(None),
6472                #[cfg(test)]
6473                test_fail_post_stop_unregister_after_fence: StdMutex::new(None),
6474                #[cfg(any(test, feature = "test-support"))]
6475                test_runtime_loop_before_queue_authority: StdMutex::new(None),
6476                #[cfg(test)]
6477                test_control_command_after_logical_lookup: StdMutex::new(None),
6478            }),
6479        }
6480    }
6481
6482    /// Create a persistent adapter with a RuntimeStore.
6483    ///
6484    /// One logical runtime id must have exactly one live `MeerkatMachine`
6485    /// authority. Separate machines may partition one store across distinct
6486    /// runtime ids, but must not concurrently register the same session. Share
6487    /// this machine (normally through `Arc`) when composing multiple surfaces.
6488    pub fn persistent(store: Arc<dyn RuntimeStore>, blob_store: Arc<dyn BlobStore>) -> Self {
6489        #[cfg(not(target_arch = "wasm32"))]
6490        let (auth_lease, oauth_flows) = {
6491            let authorities = persistent_auth_authorities(&store);
6492            (
6493                Arc::clone(&authorities.auth_lease),
6494                Arc::clone(&authorities.oauth_flows),
6495            )
6496        };
6497        #[cfg(target_arch = "wasm32")]
6498        let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
6499        let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
6500        Self {
6501            shared: Arc::new(MeerkatMachineShared {
6502                sessions: RwLock::new(HashMap::new()),
6503                registration_transaction_slots: StdRwLock::new(HashMap::new()),
6504                store: Some(store),
6505                blob_store: Some(blob_store),
6506                llm_reconfigure_host: StdRwLock::new(None),
6507                member_observation_host: StdRwLock::new(None),
6508                member_live_host: StdRwLock::new(None),
6509                live_commands_served: std::sync::atomic::AtomicU64::new(0),
6510                member_incarnation_slots: StdRwLock::new(HashMap::new()),
6511                auth_lease: StdRwLock::new(auth_lease),
6512                #[cfg(not(target_arch = "wasm32"))]
6513                oauth_flows: StdRwLock::new(oauth_flows),
6514                #[cfg(feature = "live")]
6515                live_unbound_rejection_authority: live_unbound_rejection_authority(),
6516                session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
6517                composition_signal_dispatcher: StdRwLock::new(None),
6518                #[cfg(feature = "test-support")]
6519                test_stop_executor_after_ensure: std::sync::atomic::AtomicBool::new(false),
6520                #[cfg(feature = "test-support")]
6521                test_pause_executor_after_ensure: std::sync::atomic::AtomicBool::new(false),
6522                #[cfg(feature = "test-support")]
6523                test_executor_after_ensure_pause_reached: crate::tokio::sync::Notify::new(),
6524                #[cfg(feature = "test-support")]
6525                test_executor_after_ensure_pause_release: crate::tokio::sync::Notify::new(),
6526                #[cfg(test)]
6527                test_fenced_accept_after_lease: StdMutex::new(None),
6528                #[cfg(test)]
6529                test_registration_transaction_contention_probe: StdMutex::new(None),
6530                #[cfg(test)]
6531                test_fail_post_stop_unregister_after_fence: StdMutex::new(None),
6532                #[cfg(any(test, feature = "test-support"))]
6533                test_runtime_loop_before_queue_authority: StdMutex::new(None),
6534                #[cfg(test)]
6535                test_control_command_after_logical_lookup: StdMutex::new(None),
6536            }),
6537        }
6538    }
6539
6540    /// Create a persistent adapter with a RuntimeStore but no blob store.
6541    ///
6542    /// The driver remains persistent for session state. Blob-backed inputs fail
6543    /// explicitly at the blob-store boundary until a real [`BlobStore`] is
6544    /// supplied. As with [`Self::persistent`], one logical runtime id must have
6545    /// exactly one live machine authority.
6546    pub fn persistent_without_blobs(store: Arc<dyn RuntimeStore>) -> Self {
6547        #[cfg(not(target_arch = "wasm32"))]
6548        let (auth_lease, oauth_flows) = {
6549            let authorities = persistent_auth_authorities(&store);
6550            (
6551                Arc::clone(&authorities.auth_lease),
6552                Arc::clone(&authorities.oauth_flows),
6553            )
6554        };
6555        #[cfg(target_arch = "wasm32")]
6556        let auth_lease = Arc::new(crate::handles::RuntimeAuthLeaseHandle::new());
6557        let auth_lease = generated_runtime_auth_lease_handle(auth_lease);
6558        Self {
6559            shared: Arc::new(MeerkatMachineShared {
6560                sessions: RwLock::new(HashMap::new()),
6561                registration_transaction_slots: StdRwLock::new(HashMap::new()),
6562                store: Some(store),
6563                blob_store: Some(Arc::new(UnavailableBlobStore)),
6564                llm_reconfigure_host: StdRwLock::new(None),
6565                member_observation_host: StdRwLock::new(None),
6566                member_live_host: StdRwLock::new(None),
6567                live_commands_served: std::sync::atomic::AtomicU64::new(0),
6568                member_incarnation_slots: StdRwLock::new(HashMap::new()),
6569                auth_lease: StdRwLock::new(auth_lease),
6570                #[cfg(not(target_arch = "wasm32"))]
6571                oauth_flows: StdRwLock::new(oauth_flows),
6572                #[cfg(feature = "live")]
6573                live_unbound_rejection_authority: live_unbound_rejection_authority(),
6574                session_claims: Arc::new(crate::handles::RuntimeSessionClaimRegistry::new()),
6575                composition_signal_dispatcher: StdRwLock::new(None),
6576                #[cfg(feature = "test-support")]
6577                test_stop_executor_after_ensure: std::sync::atomic::AtomicBool::new(false),
6578                #[cfg(feature = "test-support")]
6579                test_pause_executor_after_ensure: std::sync::atomic::AtomicBool::new(false),
6580                #[cfg(feature = "test-support")]
6581                test_executor_after_ensure_pause_reached: crate::tokio::sync::Notify::new(),
6582                #[cfg(feature = "test-support")]
6583                test_executor_after_ensure_pause_release: crate::tokio::sync::Notify::new(),
6584                #[cfg(test)]
6585                test_fenced_accept_after_lease: StdMutex::new(None),
6586                #[cfg(test)]
6587                test_registration_transaction_contention_probe: StdMutex::new(None),
6588                #[cfg(test)]
6589                test_fail_post_stop_unregister_after_fence: StdMutex::new(None),
6590                #[cfg(any(test, feature = "test-support"))]
6591                test_runtime_loop_before_queue_authority: StdMutex::new(None),
6592                #[cfg(test)]
6593                test_control_command_after_logical_lookup: StdMutex::new(None),
6594            }),
6595        }
6596    }
6597
6598    /// Shared auth lifecycle handle used by all runtime-backed session
6599    /// bindings created by this adapter.
6600    pub fn auth_lease_handle(&self) -> Arc<dyn meerkat_core::handles::AuthLeaseHandle> {
6601        self.generated_auth_lease_handle().clone_handle()
6602    }
6603
6604    /// Generated-authority-certified auth lifecycle handle used at factory and
6605    /// resolver seams that must reject arbitrary handwritten handles.
6606    pub fn generated_auth_lease_handle(&self) -> meerkat_core::handles::GeneratedAuthLeaseHandle {
6607        self.auth_lease
6608            .read()
6609            .unwrap_or_else(std::sync::PoisonError::into_inner)
6610            .clone()
6611    }
6612
6613    /// Install the auth lifecycle authority that public surfaces also read.
6614    ///
6615    /// Surfaces construct the adapter before all state fields are available, so
6616    /// this setter lets them align the adapter's runtime-backed traffic with
6617    /// the surface-visible status handle without creating a competing registry.
6618    pub fn set_auth_lease_handle(&self, handle: Arc<crate::handles::RuntimeAuthLeaseHandle>) {
6619        self.set_runtime_auth_lease_handle(handle);
6620    }
6621
6622    /// Install the runtime credential lifecycle handle together with an
6623    /// explicit OAuth login-flow authority.
6624    ///
6625    /// The credential side still has to be a generated AuthMachine authority;
6626    /// the explicit OAuth authority only controls login-flow test seams.
6627    #[cfg(not(target_arch = "wasm32"))]
6628    pub fn set_auth_lease_handle_with_oauth_flow_authority(
6629        &self,
6630        handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
6631        oauth_flows: Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority>,
6632    ) {
6633        *self
6634            .oauth_flows
6635            .write()
6636            .unwrap_or_else(std::sync::PoisonError::into_inner) = oauth_flows;
6637        let handle = generated_runtime_auth_lease_handle(handle);
6638        *self
6639            .auth_lease
6640            .write()
6641            .unwrap_or_else(std::sync::PoisonError::into_inner) = handle;
6642    }
6643
6644    /// Install a runtime AuthMachine authority shared by auth leases and OAuth
6645    /// login-flow lifecycle transitions.
6646    pub fn set_runtime_auth_lease_handle(
6647        &self,
6648        handle: Arc<crate::handles::RuntimeAuthLeaseHandle>,
6649    ) {
6650        #[cfg(not(target_arch = "wasm32"))]
6651        {
6652            *self
6653                .oauth_flows
6654                .write()
6655                .unwrap_or_else(std::sync::PoisonError::into_inner) =
6656                Arc::new(crate::handles::RuntimeOAuthFlowHandle::new_with_auth_lease(
6657                    std::time::Duration::from_secs(10 * 60),
6658                    Arc::clone(&handle),
6659                ));
6660        }
6661        let handle = generated_runtime_auth_lease_handle(handle);
6662        *self
6663            .auth_lease
6664            .write()
6665            .unwrap_or_else(std::sync::PoisonError::into_inner) = handle;
6666    }
6667
6668    /// Shared OAuth login-flow authority used by all auth surfaces that are
6669    /// backed by this runtime adapter.
6670    #[cfg(not(target_arch = "wasm32"))]
6671    pub fn oauth_flow_authority(
6672        &self,
6673    ) -> Arc<dyn meerkat_auth_core::oauth_flow::OAuthFlowAuthority> {
6674        Arc::clone(
6675            &self
6676                .oauth_flows
6677                .read()
6678                .unwrap_or_else(std::sync::PoisonError::into_inner),
6679        )
6680    }
6681
6682    /// The canonical session-identity claim handle owned by this
6683    /// `MeerkatMachine`. Comms runtimes wired through this machine acquire
6684    /// their session-id claim through it; the registry is scoped to this
6685    /// machine instance so tests / parallel runtimes do not collide.
6686    pub fn session_claim_handle(&self) -> Arc<dyn meerkat_core::handles::SessionClaimHandle> {
6687        Arc::clone(&self.session_claims) as Arc<dyn meerkat_core::handles::SessionClaimHandle>
6688    }
6689
6690    /// Attach the typed composition signal dispatcher used for
6691    /// MeerkatMachine -> MobMachine lifecycle observation routes.
6692    pub fn set_composition_signal_dispatcher(
6693        &self,
6694        dispatcher: composition::MeerkatCompositionSignalDispatcher,
6695    ) {
6696        let mut slot = self
6697            .composition_signal_dispatcher
6698            .write()
6699            .unwrap_or_else(std::sync::PoisonError::into_inner);
6700        *slot = Some(dispatcher);
6701    }
6702
6703    /// Apply a routed-input variant delivered by the `meerkat_mob_seam`
6704    /// composition dispatcher against the session's shared DSL authority.
6705    ///
6706    /// The caller is
6707    /// [`crate::meerkat_machine::composition::MeerkatConsumerSurface::apply_routed_input`];
6708    /// it has already projected producer fields into the typed
6709    /// [`dsl::MeerkatMachineInput`] shape. This method performs the
6710    /// session lookup + DSL-lock-scoped apply. A typed transition error
6711    /// from the kernel is surfaced as a `String` so the dispatcher can
6712    /// map it onto `DispatchRefusal::ConsumerRefused`.
6713    pub(crate) async fn apply_routed_meerkat_input(
6714        &self,
6715        session_id: &SessionId,
6716        input: dsl::MeerkatMachineInput,
6717    ) -> Result<(), dsl_authority::DslTransitionRefusal> {
6718        let _gate_guard = self
6719            .lock_current_session_mutation_gate(session_id)
6720            .await
6721            .ok_or_else(|| {
6722                dsl_authority::DslTransitionRefusal::other(
6723                    "routed_session_not_registered",
6724                    format!(
6725                        "session `{session_id}` is not registered with this MeerkatMachine; \
6726                         cannot deliver routed input"
6727                    ),
6728                )
6729            })?;
6730        self.apply_routed_session_dsl_input(session_id, input, "RoutedMeerkatInput")
6731            .await
6732            .map(|_| ())
6733    }
6734
6735    #[cfg(test)]
6736    pub(crate) async fn debug_shared_ingress_authorities(
6737        &self,
6738        session_id: &SessionId,
6739    ) -> Option<(
6740        Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
6741        crate::driver::ephemeral::SharedIngressDslAuthority,
6742    )> {
6743        let sessions = self.sessions.read().await;
6744        let entry = sessions.get(session_id)?;
6745        let session_authority = Arc::clone(&entry.dsl_authority);
6746        let driver = entry.driver.lock().await;
6747        Some((session_authority, driver.shared_dsl_authority()))
6748    }
6749
6750    /// Create a driver entry for a session.
6751    fn make_driver(
6752        &self,
6753        runtime_id: LogicalRuntimeId,
6754        dsl_authority: crate::driver::ephemeral::SharedIngressDslAuthority,
6755        initial_runtime_state: RuntimeState,
6756    ) -> DriverEntry {
6757        let control_projection = Arc::new(StdRwLock::new(
6758            crate::driver::ephemeral::RuntimeControlProjection {
6759                phase: initial_runtime_state,
6760                current_run_id: None,
6761                pre_run_phase: None,
6762            },
6763        ));
6764        match (&self.store, &self.blob_store) {
6765            (Some(store), Some(blob_store)) => {
6766                DriverEntry::Persistent(PersistentRuntimeDriver::new_with_control(
6767                    runtime_id,
6768                    store.clone(),
6769                    blob_store.clone(),
6770                    control_projection,
6771                    dsl_authority,
6772                ))
6773            }
6774            _ => DriverEntry::Ephemeral(EphemeralRuntimeDriver::new_with_control_and_dsl(
6775                runtime_id,
6776                control_projection,
6777                dsl_authority,
6778            )),
6779        }
6780    }
6781
6782    /// Recover or create fresh ops lifecycle state for a session.
6783    ///
6784    /// This is the single canonical recovery seam. Both `register_session()`
6785    /// and `ensure_session_with_executor()`'s cold path call this to create
6786    /// epoch-local state. If a durable store is available, attempts to load
6787    /// the persisted snapshot; otherwise creates fresh state with a new epoch.
6788    async fn recover_or_create_ops_state(
6789        &self,
6790        session_id: &SessionId,
6791        runtime_id: &LogicalRuntimeId,
6792    ) -> Result<
6793        (
6794            Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
6795            meerkat_core::RuntimeEpochId,
6796            Arc<meerkat_core::EpochCursorState>,
6797        ),
6798        RuntimeDriverError,
6799    > {
6800        if let Some(ref store) = self.store {
6801            let (registry, epoch_id, cursor_state) = Self::fresh_ops_state();
6802            // The epoch is an owner witness embedded in durable input/outbox
6803            // records. Atomically initialize its empty authority image before
6804            // bindings can escape. A competing cold registrar may win this
6805            // boundary; in that case we recover the winner's canonical image
6806            // rather than publishing two epochs for one logical runtime.
6807            let initial_snapshot = registry
6808                .capture_persistence_snapshot(epoch_id.clone(), cursor_state.as_ref())
6809                .map_err(|error| {
6810                    RuntimeDriverError::Internal(format!(
6811                        "failed to capture initial ops lifecycle authority for session {session_id}: {error}"
6812                    ))
6813                })?;
6814            let canonical_snapshot = store
6815                .initialize_ops_lifecycle_if_absent(runtime_id, &initial_snapshot)
6816                .await
6817                .map_err(|error| {
6818                    RuntimeDriverError::Internal(format!(
6819                        "failed to initialize ops lifecycle authority for session {session_id}: {error}"
6820                    ))
6821                })?;
6822            let recovered_epoch = canonical_snapshot.epoch_id.clone();
6823            let initialized = recovered_epoch == epoch_id;
6824            let recovered_ops_count = canonical_snapshot.completion_entries.len();
6825            let registry = crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::from_recovered(
6826                canonical_snapshot,
6827            )
6828            .map_err(|error| {
6829                tracing::error!(
6830                    %session_id,
6831                    %runtime_id,
6832                    error = %error,
6833                    "failed to recover ops lifecycle through generated authority"
6834                );
6835                RuntimeDriverError::Internal(format!(
6836                    "failed to recover ops lifecycle through generated authority: {error}"
6837                ))
6838            })?;
6839            let recovered_cursor_snapshot = registry.completion_cursor_snapshot();
6840            let recovered_cursors = meerkat_core::EpochCursorState::from_recovered(
6841                recovered_cursor_snapshot.agent_applied_cursor,
6842                recovered_cursor_snapshot.runtime_observed_seq,
6843                recovered_cursor_snapshot.runtime_last_injected_seq,
6844            );
6845            tracing::info!(
6846                %session_id,
6847                %runtime_id,
6848                epoch_id = %recovered_epoch,
6849                recovered_ops = recovered_ops_count,
6850                initialized,
6851                "ops lifecycle authority selected from durable store"
6852            );
6853            Ok((
6854                Arc::new(registry),
6855                recovered_epoch,
6856                Arc::new(recovered_cursors),
6857            ))
6858        } else {
6859            Ok((
6860                Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new()),
6861                meerkat_core::RuntimeEpochId::new(),
6862                Arc::new(meerkat_core::EpochCursorState::new()),
6863            ))
6864        }
6865    }
6866
6867    fn fresh_ops_state() -> (
6868        Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
6869        meerkat_core::RuntimeEpochId,
6870        Arc<meerkat_core::EpochCursorState>,
6871    ) {
6872        let registry = Arc::new(crate::ops_lifecycle::RuntimeOpsLifecycleRegistry::new());
6873        let epoch = meerkat_core::RuntimeEpochId::new();
6874        let cursors = Arc::new(meerkat_core::EpochCursorState::new());
6875        (registry, epoch, cursors)
6876    }
6877
6878    #[allow(clippy::large_futures)]
6879    fn execute_meerkat_machine_command(
6880        &self,
6881        self_handle: Option<Arc<Self>>,
6882        command: MeerkatMachineCommand,
6883    ) -> MeerkatMachineCommandFuture<'_> {
6884        Box::pin(async move {
6885            match command {
6886                MeerkatMachineCommand::EnsureSessionWithExecutor { .. } => {
6887                    let self_handle = self_handle.ok_or_else(|| {
6888                        MeerkatMachineCommandError::Driver(RuntimeDriverError::Internal(
6889                            "EnsureSessionWithExecutor requires Arc<Self> machine handle".into(),
6890                        ))
6891                    })?;
6892                    self_handle
6893                        .execute_meerkat_machine_ensure_session_command(command)
6894                        .await
6895                        .map_err(Into::into)
6896                }
6897                MeerkatMachineCommand::RegisterSession { .. }
6898                | MeerkatMachineCommand::UnregisterSession { .. }
6899                | MeerkatMachineCommand::SetSilentIntents { .. }
6900                | MeerkatMachineCommand::CancelAfterBoundary { .. }
6901                | MeerkatMachineCommand::StopRuntimeExecutor { .. }
6902                | MeerkatMachineCommand::CommitServiceTurnTerminalReceipt { .. }
6903                | MeerkatMachineCommand::ContainsSession { .. }
6904                | MeerkatMachineCommand::SessionHasExecutor { .. }
6905                | MeerkatMachineCommand::SessionHasComms { .. }
6906                | MeerkatMachineCommand::OpsLifecycleRegistry { .. }
6907                | MeerkatMachineCommand::PrepareBindings { .. }
6908                | MeerkatMachineCommand::PrepareLocalSessionBindings { .. }
6909                | MeerkatMachineCommand::InputState { .. }
6910                | MeerkatMachineCommand::InputStateByIdempotencyKey { .. }
6911                | MeerkatMachineCommand::InteractionTerminalStatus { .. }
6912                | MeerkatMachineCommand::RunTerminalStatus { .. }
6913                | MeerkatMachineCommand::ListActiveInputs { .. }
6914                | MeerkatMachineCommand::ReconfigureSessionLlmIdentity { .. }
6915                | MeerkatMachineCommand::StagePersistentFilter { .. }
6916                | MeerkatMachineCommand::RequestDeferredTools { .. }
6917                | MeerkatMachineCommand::PublishCommittedVisibleSet { .. } => self
6918                    .execute_meerkat_machine_session_command(command)
6919                    .await
6920                    .map_err(Into::into),
6921                MeerkatMachineCommand::SetPeerIngressContext { .. }
6922                | MeerkatMachineCommand::NotifyDrainExited { .. } => {
6923                    let self_handle = self_handle.ok_or_else(|| {
6924                        MeerkatMachineCommandError::Driver(RuntimeDriverError::Internal(
6925                            "drain command requires Arc<Self> machine handle".into(),
6926                        ))
6927                    })?;
6928                    self_handle
6929                        .execute_meerkat_machine_drain_command(command)
6930                        .await
6931                        .map_err(Into::into)
6932                }
6933                MeerkatMachineCommand::AbortAll
6934                | MeerkatMachineCommand::Abort { .. }
6935                | MeerkatMachineCommand::Wait { .. } => self
6936                    .execute_meerkat_machine_drain_local_command(command)
6937                    .await
6938                    .map_err(Into::into),
6939                MeerkatMachineCommand::Ingest { .. }
6940                | MeerkatMachineCommand::PublishEvent { .. }
6941                | MeerkatMachineCommand::Retire { .. }
6942                | MeerkatMachineCommand::Recycle { .. }
6943                | MeerkatMachineCommand::Reset { .. }
6944                | MeerkatMachineCommand::Recover { .. }
6945                | MeerkatMachineCommand::Destroy { .. }
6946                | MeerkatMachineCommand::RuntimeState { .. }
6947                | MeerkatMachineCommand::ResolvedSessionLlmCapabilities { .. }
6948                | MeerkatMachineCommand::ConfigureModelRoutingBaseline { .. }
6949                | MeerkatMachineCommand::SessionModelRoutingStatus { .. }
6950                | MeerkatMachineCommand::RequestSwitchTurn { .. }
6951                | MeerkatMachineCommand::AdmitModelRoutingAssistantTurn { .. }
6952                | MeerkatMachineCommand::BeginImageOperation { .. }
6953                | MeerkatMachineCommand::DenyImageOperationPlan { .. }
6954                | MeerkatMachineCommand::ActivateImageOperationOverride { .. }
6955                | MeerkatMachineCommand::ClassifyImageOperationTerminal { .. }
6956                | MeerkatMachineCommand::CompleteImageOperation { .. }
6957                | MeerkatMachineCommand::RestoreImageOperationOverride { .. }
6958                | MeerkatMachineCommand::LoadBoundaryReceipt { .. } => self
6959                    .execute_meerkat_machine_control_command(command)
6960                    .await
6961                    .map_err(Into::into),
6962                MeerkatMachineCommand::AcceptWithCompletion { .. }
6963                | MeerkatMachineCommand::AcceptWithoutWake { .. } => self
6964                    .execute_meerkat_machine_ingress_command(command)
6965                    .await
6966                    .map_err(Into::into),
6967            }
6968        })
6969    }
6970
6971    /// Register a runtime driver for a session (no RuntimeLoop — inputs queue but
6972    /// nothing processes them automatically). Useful for tests and legacy mode.
6973    ///
6974    /// Registration is a control-plane prerequisite: a failed register must not be
6975    /// laundered to success. The inner command can fail recovery, so the typed
6976    /// error is propagated to the caller rather than discarded.
6977    pub async fn register_session(
6978        &self,
6979        session_id: SessionId,
6980    ) -> Result<(), RuntimeControlPlaneError> {
6981        match self
6982            .execute_meerkat_machine_command(
6983                None,
6984                MeerkatMachineCommand::RegisterSession { session_id },
6985            )
6986            .await
6987            .map_err(MeerkatMachine::control_plane_error_from_command_error)?
6988        {
6989            MeerkatMachineCommandResult::Unit => Ok(()),
6990            other => Err(RuntimeControlPlaneError::Internal(format!(
6991                "register_session: unexpected command result variant: {other:?}"
6992            ))),
6993        }
6994    }
6995}
6996
6997#[cfg(test)]
6998#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
6999#[path = "../meerkat_machine_tests.rs"]
7000mod tests;