Skip to main content

meerkat_runtime/meerkat_machine/
session_management.rs

1use super::*;
2
3type OpsLifecyclePersistenceReceiver = crate::tokio::sync::mpsc::UnboundedReceiver<
4    crate::ops_lifecycle::OpsLifecyclePersistenceRequest,
5>;
6
7#[derive(Debug, Clone)]
8struct RuntimeOpsLifecycleDurabilityAuthority {
9    action: crate::meerkat_machine::dsl::RuntimeOpsLifecycleDurabilityAction,
10}
11
12#[derive(Debug, Clone)]
13struct RuntimeLifecycleRecoveryObservation {
14    runtime_state: RuntimeState,
15    agent_runtime_id: Option<LogicalRuntimeId>,
16    fence_token: Option<u64>,
17    runtime_generation: Option<crate::meerkat_machine::dsl::Generation>,
18    runtime_epoch_id: Option<crate::meerkat_machine::dsl::RuntimeEpochId>,
19    recovered_from_snapshot: bool,
20}
21
22impl RuntimeLifecycleRecoveryObservation {
23    fn from_snapshot(snapshot: Option<crate::store::MachineLifecycleSnapshot>) -> Self {
24        let Some(snapshot) = snapshot else {
25            return Self {
26                runtime_state: RuntimeState::Idle,
27                agent_runtime_id: None,
28                fence_token: None,
29                runtime_generation: None,
30                runtime_epoch_id: None,
31                recovered_from_snapshot: false,
32            };
33        };
34        let binding = snapshot.binding();
35        Self {
36            runtime_state: snapshot.runtime_state(),
37            agent_runtime_id: binding
38                .agent_runtime_id()
39                .map(|value| LogicalRuntimeId::new(value.to_owned())),
40            fence_token: binding.fence_token(),
41            runtime_generation: binding
42                .runtime_generation()
43                .map(crate::meerkat_machine::dsl::Generation::from),
44            runtime_epoch_id: binding
45                .runtime_epoch_id()
46                .map(crate::meerkat_machine::dsl::RuntimeEpochId::from),
47            recovered_from_snapshot: true,
48        }
49    }
50
51    fn requires_observed_recovery(&self) -> bool {
52        self.recovered_from_snapshot
53            && (self.runtime_state != RuntimeState::Idle
54                || self.agent_runtime_id.is_some()
55                || self.fence_token.is_some()
56                || self.runtime_generation.is_some()
57                || self.runtime_epoch_id.is_some())
58    }
59}
60
61fn fresh_registered_runtime_authority(
62    session_id: &SessionId,
63    context: &'static str,
64) -> Result<crate::meerkat_machine::dsl::MeerkatMachineAuthority, RuntimeDriverError> {
65    let mut authority = super::dsl_authority::new_initialized_authority(context);
66    crate::meerkat_machine::dsl::MeerkatMachineMutator::apply(
67        &mut authority,
68        crate::meerkat_machine::dsl::MeerkatMachineInput::RegisterSession {
69            session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
70        },
71    )
72    .map_err(|err| {
73        RuntimeDriverError::Internal(super::dsl_authority::map_error(
74            err,
75            "fresh session registration",
76        ))
77    })?;
78    Ok(authority)
79}
80
81fn runtime_ops_lifecycle_durability_authority_from_effects(
82    session_id: &SessionId,
83    effects: &[crate::meerkat_machine::dsl::MeerkatMachineEffect],
84) -> Result<RuntimeOpsLifecycleDurabilityAuthority, RuntimeDriverError> {
85    let expected_session_id = crate::meerkat_machine::dsl::SessionId::from_domain(session_id);
86    effects
87        .iter()
88        .find_map(|effect| match effect {
89            crate::meerkat_machine::dsl::MeerkatMachineEffect::RuntimeOpsLifecycleDurabilityResolved {
90                session_id,
91                action,
92                ..
93            } if session_id == &expected_session_id => {
94                Some(RuntimeOpsLifecycleDurabilityAuthority { action: *action })
95            }
96            _ => None,
97        })
98        .ok_or_else(|| {
99            RuntimeDriverError::Internal(format!(
100                "UnregisterSession for session '{session_id}' emitted no RuntimeOpsLifecycleDurabilityResolved effect"
101            ))
102        })
103}
104
105async fn persist_ops_lifecycle_request(
106    store: &Arc<dyn RuntimeStore>,
107    runtime_id: &LogicalRuntimeId,
108    request: crate::ops_lifecycle::OpsLifecyclePersistenceRequest,
109) {
110    let result = store
111        .persist_ops_lifecycle(runtime_id, request.snapshot())
112        .await
113        .map_err(|error| {
114            meerkat_core::ops_lifecycle::OpsLifecycleError::Internal(format!(
115                "failed to persist ops lifecycle snapshot: {error}"
116            ))
117        });
118    if let Err(error) = &result {
119        tracing::warn!(
120            %runtime_id,
121            error = %error,
122            "failed to persist ops lifecycle snapshot"
123        );
124    }
125    request.complete(result);
126}
127
128#[cfg(not(target_arch = "wasm32"))]
129fn spawn_ops_lifecycle_persistence_worker(
130    store: Arc<dyn RuntimeStore>,
131    runtime_id: LogicalRuntimeId,
132    mut persist_rx: OpsLifecyclePersistenceReceiver,
133) {
134    let thread_name = format!("ops-lifecycle-persist-{runtime_id}");
135    let worker_runtime_id = runtime_id.clone();
136    let spawn_result = std::thread::Builder::new()
137        .name(thread_name)
138        .spawn(move || {
139            let runtime = match crate::tokio::runtime::Builder::new_current_thread()
140                .enable_all()
141                .build()
142            {
143                Ok(runtime) => runtime,
144                Err(error) => {
145                    tracing::error!(
146                        %worker_runtime_id,
147                        error = %error,
148                        "failed to start ops lifecycle persistence worker runtime"
149                    );
150                    return;
151                }
152            };
153            runtime.block_on(async move {
154                while let Some(request) = persist_rx.recv().await {
155                    persist_ops_lifecycle_request(&store, &worker_runtime_id, request).await;
156                }
157            });
158        });
159    if let Err(error) = spawn_result {
160        tracing::error!(
161            %runtime_id,
162            error = %error,
163            "failed to spawn ops lifecycle persistence worker"
164        );
165    }
166}
167
168#[cfg(target_arch = "wasm32")]
169fn spawn_ops_lifecycle_persistence_worker(
170    store: Arc<dyn RuntimeStore>,
171    runtime_id: LogicalRuntimeId,
172    mut persist_rx: OpsLifecyclePersistenceReceiver,
173) {
174    crate::tokio::spawn(async move {
175        while let Some(request) = persist_rx.recv().await {
176            persist_ops_lifecycle_request(&store, &runtime_id, request).await;
177        }
178    });
179}
180
181impl MeerkatMachine {
182    async fn durable_lifecycle_for_registration(
183        &self,
184        runtime_id: &LogicalRuntimeId,
185    ) -> Result<Option<crate::store::MachineLifecycleSnapshot>, RuntimeDriverError> {
186        let Some(store) = self.store.as_ref() else {
187            return Ok(None);
188        };
189        crate::store::load_machine_lifecycle(store.as_ref(), runtime_id)
190            .await
191            .map_err(|err| RuntimeDriverError::Internal(err.to_string()))
192    }
193
194    pub(super) async fn register_session_inner(
195        &self,
196        session_id: SessionId,
197    ) -> Result<bool, RuntimeDriverError> {
198        let storeless = self.store.is_none();
199        tracing::debug!(%session_id, storeless, "MeerkatMachine::register_session_inner start");
200        #[cfg(target_arch = "wasm32")]
201        if storeless {
202            {
203                tracing::debug!(%session_id, "MeerkatMachine::register_session_inner attempting storeless existing check lock");
204                let mut sessions = self.sessions.try_write().map_err(|_| {
205                    tracing::warn!(
206                        %session_id,
207                        "storeless session map busy while checking existing registration"
208                    );
209                    RuntimeDriverError::Internal(format!(
210                        "storeless session map busy while registering {session_id}"
211                    ))
212                })?;
213                tracing::debug!(%session_id, "MeerkatMachine::register_session_inner locked storeless existing check");
214                if let Some(existing) = sessions.get_mut(&session_id) {
215                    tracing::debug!(
216                        %session_id,
217                        "MeerkatMachine::register_session_inner found existing session"
218                    );
219                    if existing.clear_dead_attachment() {
220                        existing.stage_generated_executor_exit_observation().map_err(|reason| {
221                            RuntimeDriverError::Internal(format!(
222                                "generated MeerkatMachine rejected executor-exit observation: {reason}"
223                            ))
224                        })?;
225                    }
226                    return Ok(false);
227                }
228            }
229            return self.register_storeless_session_inner_sync_build_step(session_id);
230        }
231        #[cfg(not(target_arch = "wasm32"))]
232        if storeless {
233            return Box::pin(self.register_storeless_session_inner(session_id)).await;
234        }
235        Box::pin(self.register_session_inner_impl(session_id)).await
236    }
237
238    #[cfg(target_arch = "wasm32")]
239    #[inline(never)]
240    #[allow(dead_code)]
241    fn register_storeless_session_inner_sync(
242        &self,
243        session_id: SessionId,
244    ) -> Result<bool, RuntimeDriverError> {
245        tracing::debug!(%session_id, "MeerkatMachine::register_storeless_session_inner_sync start");
246        {
247            tracing::debug!(%session_id, "MeerkatMachine::register_storeless_session_inner_sync attempting existing check lock");
248            let mut sessions = self.sessions.try_write().map_err(|_| {
249                tracing::warn!(
250                    %session_id,
251                    "storeless session map busy while checking existing registration"
252                );
253                RuntimeDriverError::Internal(format!(
254                    "storeless session map busy while registering {session_id}"
255                ))
256            })?;
257            tracing::debug!(%session_id, "MeerkatMachine::register_storeless_session_inner_sync locked existing check");
258            if let Some(existing) = sessions.get_mut(&session_id) {
259                tracing::debug!(
260                    %session_id,
261                    "MeerkatMachine::register_session_inner found existing session"
262                );
263                if existing.clear_dead_attachment() {
264                    existing.stage_generated_executor_exit_observation().map_err(|reason| {
265                        RuntimeDriverError::Internal(format!(
266                            "generated MeerkatMachine rejected executor-exit observation: {reason}"
267                        ))
268                    })?;
269                }
270                return Ok(false);
271            }
272        }
273        self.register_storeless_session_inner_sync_build_step(session_id)
274    }
275
276    #[cfg(target_arch = "wasm32")]
277    #[inline(never)]
278    pub(super) fn register_storeless_session_inner_sync_build_step(
279        &self,
280        session_id: SessionId,
281    ) -> Result<bool, RuntimeDriverError> {
282        let (runtime_id, session_entry) = self.make_storeless_session_entry_sync(&session_id)?;
283        self.insert_storeless_session_sync(session_id, runtime_id, session_entry)
284    }
285
286    #[cfg(target_arch = "wasm32")]
287    #[inline(never)]
288    fn make_storeless_session_entry_sync(
289        &self,
290        session_id: &SessionId,
291    ) -> Result<(LogicalRuntimeId, RuntimeSessionEntry), RuntimeDriverError> {
292        let runtime_id = Self::logical_runtime_id(session_id);
293        let recovered_authority =
294            fresh_registered_runtime_authority(session_id, "fresh storeless session registration")?;
295        let initial_runtime_state =
296            super::dsl_authority::runtime_phase_from_authority(&recovered_authority);
297        let dsl_authority = Arc::new(std::sync::Mutex::new(recovered_authority));
298        let entry = self.make_driver(
299            runtime_id.clone(),
300            Arc::clone(&dsl_authority),
301            initial_runtime_state,
302        );
303        let control_projection = entry.control_projection_handle();
304        let (ops_lifecycle, epoch_id, cursor_state) = Self::fresh_ops_state();
305        let handle_teardown_gate = crate::handles::HandleTeardownGate::open();
306        let tool_visibility_owner = Arc::new(MachineToolVisibilityOwner::new());
307        tool_visibility_owner.bind_dsl_authority(Arc::clone(&dsl_authority));
308        let session_entry = RuntimeSessionEntry {
309            runtime_id: runtime_id.clone(),
310            mutation_gate: Arc::new(Mutex::new(())),
311            control_projection,
312            driver: Arc::new(Mutex::new(entry)),
313            ops_lifecycle,
314            epoch_id,
315            handle_teardown_gate,
316            cursor_state,
317            completions: Arc::new(Mutex::new(crate::completion::CompletionRegistry::new())),
318            tool_visibility_owner,
319            attachment_slot: RuntimeLoopAttachmentSlot::Empty,
320            provisional_interrupt_handle: None,
321            dsl_authority,
322            drain_slot: CommsDrainSlot::new(),
323        };
324        Ok((runtime_id, session_entry))
325    }
326
327    #[cfg(target_arch = "wasm32")]
328    #[inline(never)]
329    fn insert_storeless_session_sync(
330        &self,
331        session_id: SessionId,
332        runtime_id: LogicalRuntimeId,
333        session_entry: RuntimeSessionEntry,
334    ) -> Result<bool, RuntimeDriverError> {
335        let mut sessions = self.sessions.try_write().map_err(|_| {
336            tracing::warn!(
337                %session_id,
338                "storeless session map busy while inserting registration"
339            );
340            RuntimeDriverError::Internal(format!(
341                "storeless session map busy while inserting {session_id}"
342            ))
343        })?;
344        tracing::debug!(%session_id, "MeerkatMachine::register_storeless_session_inner_sync locked insert");
345        if let Some(existing) = sessions.get_mut(&session_id) {
346            if existing.clear_dead_attachment() {
347                existing
348                    .stage_generated_executor_exit_observation()
349                    .map_err(|reason| {
350                        RuntimeDriverError::Internal(format!(
351                            "generated MeerkatMachine rejected executor-exit observation: {reason}"
352                        ))
353                    })?;
354            }
355            Ok(false)
356        } else {
357            sessions.insert(session_id, session_entry);
358            tracing::debug!(
359                %runtime_id,
360                "MeerkatMachine::register_session_inner inserted storeless session"
361            );
362            Ok(true)
363        }
364    }
365
366    #[cfg(not(target_arch = "wasm32"))]
367    async fn register_storeless_session_inner(
368        &self,
369        session_id: SessionId,
370    ) -> Result<bool, RuntimeDriverError> {
371        #[cfg(target_arch = "wasm32")]
372        {
373            let mut sessions = self.sessions.try_write().map_err(|_| {
374                RuntimeDriverError::Internal(format!(
375                    "storeless session map busy while registering {session_id}"
376                ))
377            })?;
378            if let Some(existing) = sessions.get_mut(&session_id) {
379                tracing::debug!(
380                    %session_id,
381                    "MeerkatMachine::register_session_inner found existing session"
382                );
383                if existing.clear_dead_attachment() {
384                    existing.stage_generated_executor_exit_observation().map_err(|reason| {
385                        RuntimeDriverError::Internal(format!(
386                            "generated MeerkatMachine rejected executor-exit observation: {reason}"
387                        ))
388                    })?;
389                }
390                return Ok(false);
391            }
392        }
393        #[cfg(not(target_arch = "wasm32"))]
394        {
395            let mut sessions = self.sessions.write().await;
396            if let Some(existing) = sessions.get_mut(&session_id) {
397                tracing::debug!(
398                    %session_id,
399                    "MeerkatMachine::register_session_inner found existing session"
400                );
401                if existing.clear_dead_attachment() {
402                    existing.stage_generated_executor_exit_observation().map_err(|reason| {
403                        RuntimeDriverError::Internal(format!(
404                            "generated MeerkatMachine rejected executor-exit observation: {reason}"
405                        ))
406                    })?;
407                }
408                return Ok(false);
409            }
410        }
411
412        let runtime_id = Self::logical_runtime_id(&session_id);
413        let recovered_authority = fresh_registered_runtime_authority(
414            &session_id,
415            "fresh storeless session registration",
416        )?;
417        let initial_runtime_state =
418            super::dsl_authority::runtime_phase_from_authority(&recovered_authority);
419        let dsl_authority = Arc::new(std::sync::Mutex::new(recovered_authority));
420        let mut entry = self.make_driver(
421            runtime_id.clone(),
422            Arc::clone(&dsl_authority),
423            initial_runtime_state,
424        );
425        tracing::debug!(
426            %session_id,
427            %runtime_id,
428            "MeerkatMachine::register_session_inner recovering storeless driver"
429        );
430        if let Err(err) = entry.as_driver_mut().recover().await {
431            tracing::error!(%session_id, error = %err, "failed to recover runtime driver during registration");
432            return Err(err);
433        }
434        let control_projection = entry.control_projection_handle();
435
436        let (ops_lifecycle, epoch_id, cursor_state) = Self::fresh_ops_state();
437        let handle_teardown_gate = crate::handles::HandleTeardownGate::open();
438        let tool_visibility_owner = Arc::new(MachineToolVisibilityOwner::new());
439        tool_visibility_owner.bind_dsl_authority(Arc::clone(&dsl_authority));
440        let session_entry = RuntimeSessionEntry {
441            runtime_id: runtime_id.clone(),
442            mutation_gate: Arc::new(Mutex::new(())),
443            control_projection,
444            driver: Arc::new(Mutex::new(entry)),
445            ops_lifecycle,
446            epoch_id,
447            handle_teardown_gate,
448            cursor_state,
449            completions: Arc::new(Mutex::new(crate::completion::CompletionRegistry::new())),
450            tool_visibility_owner,
451            attachment_slot: RuntimeLoopAttachmentSlot::Empty,
452            provisional_interrupt_handle: None,
453            dsl_authority,
454            drain_slot: CommsDrainSlot::new(),
455        };
456        #[cfg(target_arch = "wasm32")]
457        {
458            let mut sessions = self.sessions.try_write().map_err(|_| {
459                RuntimeDriverError::Internal(format!(
460                    "storeless session map busy while inserting {session_id}"
461                ))
462            })?;
463            if let Some(existing) = sessions.get_mut(&session_id) {
464                if existing.clear_dead_attachment() {
465                    existing
466                        .stage_generated_executor_exit_observation()
467                        .map_err(|reason| {
468                            RuntimeDriverError::Internal(format!(
469                                "generated MeerkatMachine rejected executor-exit observation: {reason}"
470                            ))
471                        })?;
472                }
473                Ok(false)
474            } else {
475                sessions.insert(session_id, session_entry);
476                tracing::debug!(
477                    %runtime_id,
478                    "MeerkatMachine::register_session_inner inserted storeless session"
479                );
480                Ok(true)
481            }
482        }
483        #[cfg(not(target_arch = "wasm32"))]
484        {
485            let mut sessions = self.sessions.write().await;
486            if let Some(existing) = sessions.get_mut(&session_id) {
487                if existing.clear_dead_attachment() {
488                    existing
489                        .stage_generated_executor_exit_observation()
490                        .map_err(|reason| {
491                            RuntimeDriverError::Internal(format!(
492                                "generated MeerkatMachine rejected executor-exit observation: {reason}"
493                            ))
494                        })?;
495                }
496                Ok(false)
497            } else {
498                sessions.insert(session_id, session_entry);
499                tracing::debug!(
500                    %runtime_id,
501                    "MeerkatMachine::register_session_inner inserted storeless session"
502                );
503                Ok(true)
504            }
505        }
506    }
507
508    async fn register_session_inner_impl(
509        &self,
510        session_id: SessionId,
511    ) -> Result<bool, RuntimeDriverError> {
512        {
513            let mut sessions = self.sessions.write().await;
514            if let Some(existing) = sessions.get_mut(&session_id) {
515                tracing::debug!(
516                    %session_id,
517                    "MeerkatMachine::register_session_inner found existing session"
518                );
519                if existing.clear_dead_attachment() {
520                    existing.stage_generated_executor_exit_observation().map_err(|reason| {
521                        RuntimeDriverError::Internal(format!(
522                            "generated MeerkatMachine rejected executor-exit observation: {reason}"
523                        ))
524                    })?;
525                }
526                return Ok(false);
527            }
528        }
529
530        let runtime_id = Self::logical_runtime_id(&session_id);
531        tracing::debug!(
532            %session_id,
533            %runtime_id,
534            "MeerkatMachine::register_session_inner loading durable lifecycle"
535        );
536        let recovery_observation = RuntimeLifecycleRecoveryObservation::from_snapshot(
537            self.durable_lifecycle_for_registration(&runtime_id).await?,
538        );
539        tracing::debug!(
540            %session_id,
541            %runtime_id,
542            "MeerkatMachine::register_session_inner loaded durable lifecycle"
543        );
544        let observed_runtime_state = recovery_observation.runtime_state;
545        let requires_observed_recovery = recovery_observation.requires_observed_recovery();
546        let recovered_authority = if requires_observed_recovery {
547            super::dsl_authority::recover_authority_from_runtime_observation(
548                &session_id,
549                observed_runtime_state,
550                recovery_observation.agent_runtime_id.as_ref(),
551                None,
552                None,
553                std::collections::BTreeSet::new(),
554                recovery_observation.fence_token,
555                recovery_observation.runtime_generation,
556                recovery_observation.runtime_epoch_id,
557            )
558            .map_err(|err| {
559                RuntimeDriverError::Internal(super::dsl_authority::map_error(
560                    err,
561                    "session registration DSL recovery",
562                ))
563            })?
564        } else {
565            fresh_registered_runtime_authority(&session_id, "fresh session registration")?
566        };
567        // Seed the driver's initial phase from the recovered DSL authority
568        // uniformly (same as the storeless paths): the authority is the owner;
569        // the driver control projection mirrors it, never the raw observation.
570        let initial_runtime_state =
571            super::dsl_authority::runtime_phase_from_authority(&recovered_authority);
572        let dsl_authority = Arc::new(std::sync::Mutex::new(recovered_authority));
573        tracing::debug!(
574            %session_id,
575            %runtime_id,
576            ?initial_runtime_state,
577            "MeerkatMachine::register_session_inner recovered authority"
578        );
579        let mut entry = self.make_driver(
580            runtime_id.clone(),
581            Arc::clone(&dsl_authority),
582            initial_runtime_state,
583        );
584        tracing::debug!(
585            %session_id,
586            %runtime_id,
587            "MeerkatMachine::register_session_inner recovering driver"
588        );
589        if let Err(err) = entry.as_driver_mut().recover().await {
590            tracing::error!(%session_id, error = %err, "failed to recover runtime driver during registration");
591            return Err(err);
592        }
593        tracing::debug!(
594            %session_id,
595            %runtime_id,
596            "MeerkatMachine::register_session_inner recovered driver"
597        );
598        let control_projection = entry.control_projection_handle();
599
600        tracing::debug!(
601            %session_id,
602            %runtime_id,
603            "MeerkatMachine::register_session_inner recovering ops state"
604        );
605        let (ops_lifecycle, epoch_id, cursor_state) = if self.store.is_some()
606            || (requires_observed_recovery && initial_runtime_state != RuntimeState::Idle)
607        {
608            self.recover_or_create_ops_state(&session_id, &runtime_id)
609                .await?
610        } else {
611            Self::fresh_ops_state()
612        };
613        tracing::debug!(
614            %session_id,
615            %runtime_id,
616            %epoch_id,
617            "MeerkatMachine::register_session_inner recovered ops state"
618        );
619
620        let tool_visibility_owner = Arc::new(MachineToolVisibilityOwner::new());
621        // Bind the DSL authority into the visibility owner so its staging
622        // trait calls route through the canonical DSL counter
623        // `next_staged_visibility_revision` (dogma round 4, wave 2b #12).
624        tool_visibility_owner.bind_dsl_authority(Arc::clone(&dsl_authority));
625        let handle_teardown_gate = crate::handles::HandleTeardownGate::open();
626        let session_entry = RuntimeSessionEntry {
627            runtime_id: runtime_id.clone(),
628            mutation_gate: Arc::new(Mutex::new(())),
629            control_projection,
630            driver: Arc::new(Mutex::new(entry)),
631            ops_lifecycle,
632            epoch_id,
633            handle_teardown_gate,
634            cursor_state,
635            completions: Arc::new(Mutex::new(crate::completion::CompletionRegistry::new())),
636            tool_visibility_owner,
637            attachment_slot: RuntimeLoopAttachmentSlot::Empty,
638            provisional_interrupt_handle: None,
639            dsl_authority,
640            drain_slot: CommsDrainSlot::new(),
641        };
642        tracing::debug!(
643            %session_id,
644            %runtime_id,
645            "MeerkatMachine::register_session_inner inserting session"
646        );
647        let mut sessions = self.sessions.write().await;
648        if let Some(existing) = sessions.get_mut(&session_id) {
649            tracing::debug!(
650                %session_id,
651                %runtime_id,
652                "MeerkatMachine::register_session_inner found existing session before insert"
653            );
654            if existing.clear_dead_attachment() {
655                existing
656                    .stage_generated_executor_exit_observation()
657                    .map_err(|reason| {
658                        RuntimeDriverError::Internal(format!(
659                            "generated MeerkatMachine rejected executor-exit observation: {reason}"
660                        ))
661                    })?;
662            }
663            Ok(false)
664        } else {
665            sessions.insert(session_id, session_entry);
666            tracing::debug!(
667                %runtime_id,
668                "MeerkatMachine::register_session_inner inserted session"
669            );
670            Ok(true)
671        }
672    }
673
674    pub(super) async fn unregister_session_inner_if_epoch(
675        &self,
676        session_id: &SessionId,
677        epoch_id: &meerkat_core::RuntimeEpochId,
678    ) {
679        let Some(gate_guard) = self.lock_current_session_mutation_gate(session_id).await else {
680            return;
681        };
682        {
683            let sessions = self.sessions.read().await;
684            let Some(entry) = sessions.get(session_id) else {
685                return;
686            };
687            if &entry.epoch_id != epoch_id {
688                return;
689            }
690        }
691        if let Err(err) = self
692            .unregister_session_inner_locked_authorized(session_id, gate_guard)
693            .await
694        {
695            tracing::warn!(
696                %session_id,
697                error = %err,
698                "generated MeerkatMachine rejected epoch-scoped session unregister"
699            );
700        }
701    }
702
703    /// Set the silent comms intents for a session's runtime driver.
704    ///
705    /// Peer requests whose intent matches one of these strings will be accepted
706    /// without triggering an LLM turn (ApplyMode::Ignore, WakeMode::None).
707    pub async fn set_session_silent_intents(
708        &self,
709        session_id: &SessionId,
710        intents: Vec<String>,
711    ) -> Result<(), RuntimeDriverError> {
712        match self
713            .execute_meerkat_machine_command(
714                None,
715                MeerkatMachineCommand::SetSilentIntents {
716                    session_id: session_id.clone(),
717                    intents,
718                },
719            )
720            .await
721            .map_err(MeerkatMachine::driver_error_from_command_error)?
722        {
723            MeerkatMachineCommandResult::Unit => Ok(()),
724            other => Err(RuntimeDriverError::Internal(format!(
725                "set_session_silent_intents: unexpected command result variant: {other:?}"
726            ))),
727        }
728    }
729
730    pub async fn commit_service_turn_terminal_receipt(
731        &self,
732        session_id: &SessionId,
733    ) -> Result<(), RuntimeDriverError> {
734        match self
735            .execute_meerkat_machine_command(
736                None,
737                MeerkatMachineCommand::CommitServiceTurnTerminalReceipt {
738                    session_id: session_id.clone(),
739                },
740            )
741            .await
742            .map_err(|err| match err {
743                MeerkatMachineCommandError::Driver(err) => err,
744                MeerkatMachineCommandError::Control(err) => {
745                    RuntimeDriverError::Internal(err.to_string())
746                }
747            })? {
748            MeerkatMachineCommandResult::Unit => Ok(()),
749            _ => Err(RuntimeDriverError::Internal(
750                "commit_service_turn_terminal_receipt: unexpected command result variant".into(),
751            )),
752        }
753    }
754
755    /// Register a runtime driver for a session WITH a RuntimeLoop backed by a
756    /// `CoreExecutor`. Takes `self: &Arc<Self>` because executor attachment is
757    /// routed through the Arc-backed command path that owns runtime-loop spawn.
758    pub async fn register_session_with_executor(
759        self: &Arc<Self>,
760        session_id: SessionId,
761        executor: Box<dyn meerkat_core::lifecycle::CoreExecutor>,
762    ) -> Result<(), RuntimeDriverError> {
763        match self
764            .execute_meerkat_machine_command(
765                Some(Arc::clone(self)),
766                MeerkatMachineCommand::EnsureSessionWithExecutor {
767                    session_id,
768                    executor,
769                },
770            )
771            .await
772            .map_err(MeerkatMachine::driver_error_from_command_error)?
773        {
774            MeerkatMachineCommandResult::Unit => Ok(()),
775            other => Err(RuntimeDriverError::Internal(format!(
776                "register_session_with_executor: unexpected command result variant: {other:?}"
777            ))),
778        }
779    }
780
781    /// Ensure a runtime driver with executor exists for the session.
782    ///
783    /// If a session was already registered without a loop, upgrade the
784    /// existing driver in place so queued inputs remain attached to the same
785    /// runtime ledger and can start draining immediately. See
786    /// `register_session_with_executor` for why this takes `self: &Arc<Self>`.
787    pub async fn ensure_session_with_executor(
788        self: &Arc<Self>,
789        session_id: SessionId,
790        executor: Box<dyn meerkat_core::lifecycle::CoreExecutor>,
791    ) -> Result<(), RuntimeDriverError> {
792        match self
793            .execute_meerkat_machine_command(
794                Some(Arc::clone(self)),
795                MeerkatMachineCommand::EnsureSessionWithExecutor {
796                    session_id,
797                    executor,
798                },
799            )
800            .await
801            .map_err(MeerkatMachine::driver_error_from_command_error)?
802        {
803            MeerkatMachineCommandResult::Unit => Ok(()),
804            other => Err(RuntimeDriverError::Internal(format!(
805                "ensure_session_with_executor: unexpected command result variant: {other:?}"
806            ))),
807        }
808    }
809
810    /// Install a temporary live interrupt handle for a prepared session before
811    /// its runtime loop executor is attached.
812    ///
813    /// Runtime-backed surfaces use this during eager session materialization:
814    /// the session service owns the first turn until `create_session` returns,
815    /// but explicit user interrupts must still route through
816    /// `MeerkatMachine::hard_cancel_current_run`.
817    pub async fn install_prepared_session_interrupt_handle(
818        &self,
819        session_id: &SessionId,
820        handle: Arc<dyn meerkat_core::lifecycle::CoreExecutorInterruptHandle>,
821    ) -> Result<(), RuntimeDriverError> {
822        let mut sessions = self.sessions.write().await;
823        let entry = sessions
824            .get_mut(session_id)
825            .ok_or(RuntimeDriverError::NotReady {
826                state: RuntimeState::Destroyed,
827            })?;
828        if entry.clear_dead_attachment() {
829            entry
830                .stage_generated_executor_exit_observation()
831                .map_err(|reason| {
832                    RuntimeDriverError::Internal(format!(
833                        "generated MeerkatMachine rejected executor-exit observation: {reason}"
834                    ))
835                })?;
836        }
837        entry.install_provisional_interrupt_handle(handle);
838        Ok(())
839    }
840
841    pub(super) async fn ensure_session_with_executor_inner(
842        self: &Arc<Self>,
843        session_id: SessionId,
844        executor: Box<dyn meerkat_core::lifecycle::CoreExecutor>,
845    ) -> Result<(), RuntimeDriverError> {
846        enum ExistingExecutorClaim {
847            AlreadyClaimed,
848            Rejected(String),
849            Claimed {
850                gate: Arc<Mutex<()>>,
851                driver: SharedDriver,
852                completions: SharedCompletionRegistry,
853                ops_lifecycle: Arc<crate::ops_lifecycle::RuntimeOpsLifecycleRegistry>,
854                dsl_authority: Arc<std::sync::Mutex<dsl::MeerkatMachineAuthority>>,
855                staged: Box<StagedSessionDslInput>,
856                repaired_dead_attachment: bool,
857                _gate_guard: crate::tokio::sync::OwnedMutexGuard<()>,
858            },
859        }
860
861        let existing = loop {
862            if let Some(gate) = self.session_mutation_gate(&session_id).await {
863                let gate_guard = Arc::clone(&gate).lock_owned().await;
864                let mut sessions = self.sessions.write().await;
865                let Some(entry) = sessions.get_mut(&session_id) else {
866                    continue;
867                };
868                if !Arc::ptr_eq(&entry.mutation_gate, &gate) {
869                    continue;
870                }
871                let repaired_dead_attachment = entry.clear_dead_attachment();
872                let repaired_deferred_stop =
873                    repaired_dead_attachment && entry.generated_stop_deferred();
874                if repaired_dead_attachment
875                    && !repaired_deferred_stop
876                    && let Err(reason) = entry.stage_generated_executor_exit_observation()
877                {
878                    break ExistingExecutorClaim::Rejected(reason);
879                }
880                if entry.generated_executor_registration_active() && !repaired_deferred_stop {
881                    break ExistingExecutorClaim::AlreadyClaimed;
882                }
883                if entry.has_live_attachment() {
884                    match entry.stage_generated_executor_registration_claim(&session_id) {
885                        Ok(_) => break ExistingExecutorClaim::AlreadyClaimed,
886                        Err(reason) => break ExistingExecutorClaim::Rejected(reason),
887                    }
888                }
889                match entry.stage_generated_executor_registration_claim(&session_id) {
890                    Ok(staged) => {
891                        break ExistingExecutorClaim::Claimed {
892                            gate,
893                            driver: entry.driver.clone(),
894                            completions: entry.completions.clone(),
895                            ops_lifecycle: entry.ops_lifecycle.clone(),
896                            dsl_authority: Arc::clone(&entry.dsl_authority),
897                            staged: Box::new(staged),
898                            repaired_dead_attachment,
899                            _gate_guard: gate_guard,
900                        };
901                    }
902                    Err(reason) => break ExistingExecutorClaim::Rejected(reason),
903                }
904            }
905
906            let runtime_id = Self::logical_runtime_id(&session_id);
907            let recovery_observation =
908                match self.durable_lifecycle_for_registration(&runtime_id).await {
909                    Ok(snapshot) => RuntimeLifecycleRecoveryObservation::from_snapshot(snapshot),
910                    Err(err) => {
911                        tracing::error!(
912                            %session_id,
913                            error = %err,
914                            "failed to load durable runtime state during executor registration"
915                        );
916                        return Err(err);
917                    }
918                };
919            let observed_runtime_state = recovery_observation.runtime_state;
920            let requires_observed_recovery = recovery_observation.requires_observed_recovery();
921            let recovered_authority = if requires_observed_recovery {
922                match super::dsl_authority::recover_authority_from_runtime_observation(
923                    &session_id,
924                    observed_runtime_state,
925                    recovery_observation.agent_runtime_id.as_ref(),
926                    None,
927                    None,
928                    std::collections::BTreeSet::new(),
929                    recovery_observation.fence_token,
930                    recovery_observation.runtime_generation,
931                    recovery_observation.runtime_epoch_id,
932                ) {
933                    Ok(authority) => authority,
934                    Err(err) => {
935                        let mapped =
936                            super::dsl_authority::map_error(err, "session recovery DSL recovery");
937                        tracing::error!(
938                            %session_id,
939                            error = %mapped,
940                            "failed to recover generated runtime authority during executor registration"
941                        );
942                        return Err(RuntimeDriverError::Internal(mapped));
943                    }
944                }
945            } else {
946                fresh_registered_runtime_authority(&session_id, "fresh executor registration")?
947            };
948            // Seed the driver's initial phase from the recovered DSL authority
949            // uniformly: the authority is the owner; the driver control
950            // projection mirrors it, never the raw observation.
951            let initial_runtime_state =
952                super::dsl_authority::runtime_phase_from_authority(&recovered_authority);
953            let dsl_authority = Arc::new(std::sync::Mutex::new(recovered_authority));
954            let mut recovered_entry = self.make_driver(
955                runtime_id.clone(),
956                Arc::clone(&dsl_authority),
957                initial_runtime_state,
958            );
959            if let Err(err) = recovered_entry.as_driver_mut().recover().await {
960                tracing::error!(
961                    %session_id,
962                    error = %err,
963                    "failed to recover runtime driver during registration"
964                );
965                return Err(err);
966            }
967            // Recover ops state OUTSIDE the sessions lock to avoid blocking
968            // other adapter operations behind potentially slow disk I/O.
969            let (recovered_ops, recovered_epoch, recovered_cursors) = if self.store.is_some()
970                || (requires_observed_recovery && initial_runtime_state != RuntimeState::Idle)
971            {
972                match self
973                    .recover_or_create_ops_state(&session_id, &runtime_id)
974                    .await
975                {
976                    Ok(recovered) => recovered,
977                    Err(err) => {
978                        tracing::error!(
979                            %session_id,
980                            error = %err,
981                            "failed to recover ops lifecycle during executor registration"
982                        );
983                        return Err(err);
984                    }
985                }
986            } else {
987                Self::fresh_ops_state()
988            };
989
990            let mutation_gate = Arc::new(Mutex::new(()));
991            let gate_guard = Arc::clone(&mutation_gate).lock_owned().await;
992            let mut sessions = self.sessions.write().await;
993            if sessions.contains_key(&session_id) {
994                continue;
995            }
996
997            let control_projection = recovered_entry.control_projection_handle();
998            let driver = Arc::new(Mutex::new(recovered_entry));
999            let completions = Arc::new(Mutex::new(crate::completion::CompletionRegistry::new()));
1000            let tool_visibility_owner = Arc::new(MachineToolVisibilityOwner::new());
1001            // Bind the DSL authority before the entry is inserted — any
1002            // subsequent staging trait call must see the bound authority.
1003            tool_visibility_owner.bind_dsl_authority(Arc::clone(&dsl_authority));
1004            sessions.insert(
1005                session_id.clone(),
1006                RuntimeSessionEntry {
1007                    runtime_id,
1008                    mutation_gate: Arc::clone(&mutation_gate),
1009                    control_projection,
1010                    driver: driver.clone(),
1011                    ops_lifecycle: recovered_ops.clone(),
1012                    epoch_id: recovered_epoch,
1013                    handle_teardown_gate: crate::handles::HandleTeardownGate::open(),
1014                    cursor_state: recovered_cursors,
1015                    completions: completions.clone(),
1016                    tool_visibility_owner,
1017                    attachment_slot: RuntimeLoopAttachmentSlot::Empty,
1018                    provisional_interrupt_handle: None,
1019                    dsl_authority: Arc::clone(&dsl_authority),
1020                    drain_slot: CommsDrainSlot::new(),
1021                },
1022            );
1023            let Some(entry) = sessions.get_mut(&session_id) else {
1024                return Err(RuntimeDriverError::Internal(format!(
1025                    "session {session_id} missing after executor recovery insert"
1026                )));
1027            };
1028            match entry.stage_generated_executor_registration_claim(&session_id) {
1029                Ok(staged) => {
1030                    break ExistingExecutorClaim::Claimed {
1031                        gate: mutation_gate,
1032                        driver,
1033                        completions,
1034                        ops_lifecycle: recovered_ops,
1035                        dsl_authority,
1036                        staged: Box::new(staged),
1037                        repaired_dead_attachment: false,
1038                        _gate_guard: gate_guard,
1039                    };
1040                }
1041                Err(reason) => {
1042                    sessions.remove(&session_id);
1043                    break ExistingExecutorClaim::Rejected(reason);
1044                }
1045            }
1046        };
1047
1048        let (
1049            driver,
1050            completions,
1051            ops_lifecycle,
1052            dsl_authority,
1053            staged_registration,
1054            repaired_dead_attachment,
1055            registration_gate,
1056            _gate_guard,
1057        ) = match existing {
1058            ExistingExecutorClaim::AlreadyClaimed => {
1059                return Ok(());
1060            }
1061            ExistingExecutorClaim::Rejected(reason) => {
1062                tracing::warn!(
1063                    %session_id,
1064                    error = %reason,
1065                    "generated MeerkatMachine rejected executor registration"
1066                );
1067                // Stage-first classification: a claim rejected on a Destroyed
1068                // binding surfaces as the terminal `Destroyed` truth.
1069                return Err(self
1070                    .classify_session_dsl_rejection(&session_id, reason)
1071                    .await);
1072            }
1073            ExistingExecutorClaim::Claimed {
1074                gate,
1075                driver,
1076                completions,
1077                ops_lifecycle,
1078                dsl_authority,
1079                staged,
1080                repaired_dead_attachment,
1081                _gate_guard,
1082            } => (
1083                driver,
1084                completions,
1085                ops_lifecycle,
1086                dsl_authority,
1087                staged,
1088                repaired_dead_attachment,
1089                gate,
1090                _gate_guard,
1091            ),
1092        };
1093
1094        let should_wake = {
1095            let mut driver_guard = driver.lock().await;
1096            driver_guard.sync_control_projection_from_dsl_authority();
1097            if repaired_dead_attachment {
1098                tracing::warn!(
1099                    %session_id,
1100                    "runtime driver registration was repaired by generated executor authority; publishing attachment"
1101                );
1102            }
1103            !driver_guard.as_driver().active_input_ids().is_empty()
1104        };
1105
1106        // Wire persistence channel if a durable store is available.
1107        if let Some(ref store) = self.store {
1108            let (persist_tx, persist_rx) = crate::tokio::sync::mpsc::unbounded_channel::<
1109                crate::ops_lifecycle::OpsLifecyclePersistenceRequest,
1110            >();
1111            let (entry_epoch_id, entry_cursor, runtime_id) = {
1112                let sessions = self.sessions.read().await;
1113                sessions.get(&session_id).map_or_else(
1114                    || {
1115                        (
1116                            meerkat_core::RuntimeEpochId::new(),
1117                            Arc::new(meerkat_core::EpochCursorState::new()),
1118                            Self::logical_runtime_id(&session_id),
1119                        )
1120                    },
1121                    |entry| {
1122                        (
1123                            entry.epoch_id.clone(),
1124                            Arc::clone(&entry.cursor_state),
1125                            entry.runtime_id.clone(),
1126                        )
1127                    },
1128                )
1129            };
1130            spawn_ops_lifecycle_persistence_worker(Arc::clone(store), runtime_id, persist_rx);
1131            ops_lifecycle.set_persistence_channel(persist_tx, entry_epoch_id, entry_cursor);
1132        }
1133
1134        // Get the completion feed from the registry for feed-based idle wake.
1135        let completion_feed = ops_lifecycle.completion_feed_handle();
1136
1137        let boundary_handle = executor.boundary_handle();
1138        let interrupt_handle = executor.interrupt_handle();
1139        let (wake_tx, wake_rx) = mpsc::channel(16);
1140        let (effect_tx, effect_rx) = mpsc::channel(16);
1141        let entry_cursor_state = {
1142            let sessions = self.sessions.read().await;
1143            sessions
1144                .get(&session_id)
1145                .map(|e| Arc::clone(&e.cursor_state))
1146        };
1147        let mut pending_loop_handle =
1148            Some(crate::runtime_loop::spawn_runtime_loop_with_completions(
1149                driver.clone(),
1150                executor,
1151                wake_rx,
1152                effect_rx,
1153                Some(completions.clone()),
1154                Some(completion_feed),
1155                Some(Arc::clone(&ops_lifecycle) as Arc<dyn meerkat_core::OpsLifecycleRegistry>),
1156                entry_cursor_state,
1157                Arc::downgrade(self),
1158                session_id.clone(),
1159            ));
1160
1161        let (published, detach_after_abort) = {
1162            let mut sessions = self.sessions.write().await;
1163            match sessions.get_mut(&session_id) {
1164                None => (false, true),
1165                Some(entry) => {
1166                    entry.clear_dead_attachment();
1167                    if entry.has_live_attachment() {
1168                        (false, false)
1169                    } else if !Arc::ptr_eq(&entry.mutation_gate, &registration_gate)
1170                        || !Arc::ptr_eq(&entry.dsl_authority, &dsl_authority)
1171                        || !Arc::ptr_eq(&entry.driver, &driver)
1172                        || !Arc::ptr_eq(&entry.completions, &completions)
1173                    {
1174                        tracing::warn!(
1175                            %session_id,
1176                            "runtime session entry changed while wiring executor; aborting stale loop attachment"
1177                        );
1178                        (false, true)
1179                    } else {
1180                        match pending_loop_handle.take() {
1181                            Some(loop_handle) => {
1182                                entry.attach_runtime_loop(
1183                                    wake_tx.clone(),
1184                                    effect_tx,
1185                                    boundary_handle,
1186                                    interrupt_handle,
1187                                    loop_handle,
1188                                );
1189                                (true, false)
1190                            }
1191                            None => {
1192                                tracing::error!(
1193                                    %session_id,
1194                                    "runtime loop handle missing during attachment publish"
1195                                );
1196                                (false, true)
1197                            }
1198                        }
1199                    }
1200                }
1201            }
1202        };
1203
1204        if !published {
1205            if let Some(loop_handle) = pending_loop_handle.take() {
1206                loop_handle.abort();
1207            }
1208            if detach_after_abort {
1209                Self::restore_dsl_authority_snapshot(
1210                    &dsl_authority,
1211                    staged_registration.previous_snapshot,
1212                );
1213                let mut driver_guard = driver.lock().await;
1214                driver_guard.sync_control_projection_from_dsl_authority();
1215                return Err(RuntimeDriverError::Internal(
1216                    "runtime session entry changed while wiring executor".into(),
1217                ));
1218            }
1219            return Ok(());
1220        }
1221
1222        if should_wake {
1223            let _ = wake_tx.try_send(());
1224        }
1225        Ok(())
1226    }
1227
1228    /// Unregister a session's runtime driver.
1229    ///
1230    /// Detaches the executor (Attached → Idle) before removal, then drops
1231    /// the wake channel sender, which causes the RuntimeLoop to exit.
1232    pub async fn unregister_session(&self, session_id: &SessionId) {
1233        self.unregister_session_inner(session_id).await;
1234    }
1235
1236    /// Stage `BeginUnregisterSession`, which opens the machine-owned drain
1237    /// window. Carries the same binding facts as the final `UnregisterSession`
1238    /// so the machine can match them against the active runtime authority.
1239    async fn stage_begin_unregister_session_authority(
1240        &self,
1241        session_id: &SessionId,
1242    ) -> Result<StagedSessionDslInput, String> {
1243        let begin_input = {
1244            let authority = self.session_dsl_authority(session_id).await?;
1245            let authority = authority
1246                .lock()
1247                .unwrap_or_else(std::sync::PoisonError::into_inner);
1248            let state = authority.state();
1249            crate::meerkat_machine::dsl::MeerkatMachineInput::BeginUnregisterSession {
1250                session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
1251                agent_runtime_id: state.active_runtime_id.clone(),
1252                fence_token: state.active_fence_token,
1253                generation: state.active_runtime_generation,
1254                runtime_epoch_id: state.active_runtime_epoch_id.clone(),
1255            }
1256        };
1257        self.stage_session_dsl_transition(session_id, begin_input, "BeginUnregisterSession")
1258            .await
1259    }
1260
1261    async fn stage_unregister_session_authority(
1262        &self,
1263        session_id: &SessionId,
1264    ) -> Result<
1265        (
1266            StagedSessionDslInput,
1267            RuntimeOpsLifecycleDurabilityAuthority,
1268        ),
1269        RuntimeDriverError,
1270    > {
1271        let (durability_input, unregister_input) = {
1272            let authority = self.session_dsl_authority(session_id).await.map_err(|reason| {
1273                RuntimeDriverError::ValidationFailed {
1274                    reason: format!(
1275                        "generated unregister authority unavailable for session {session_id}: {reason}"
1276                    ),
1277                }
1278            })?;
1279            let authority = authority
1280                .lock()
1281                .unwrap_or_else(std::sync::PoisonError::into_inner);
1282            let state = authority.state();
1283            let dsl_session_id = crate::meerkat_machine::dsl::SessionId::from_domain(session_id);
1284            let agent_runtime_id = state.active_runtime_id.clone();
1285            let fence_token = state.active_fence_token;
1286            let generation = state.active_runtime_generation;
1287            let runtime_epoch_id = state.active_runtime_epoch_id.clone();
1288            (
1289                crate::meerkat_machine::dsl::MeerkatMachineInput::ResolveRuntimeOpsLifecycleDurability {
1290                    session_id: dsl_session_id.clone(),
1291                    agent_runtime_id: agent_runtime_id.clone(),
1292                    fence_token,
1293                    generation,
1294                    runtime_epoch_id: runtime_epoch_id.clone(),
1295                },
1296                crate::meerkat_machine::dsl::MeerkatMachineInput::UnregisterSession {
1297                    session_id: dsl_session_id,
1298                    agent_runtime_id,
1299                    fence_token,
1300                    generation,
1301                    runtime_epoch_id,
1302                },
1303            )
1304        };
1305        let authority = if self.store.is_some() {
1306            let durability_effects = self
1307                .preview_session_dsl_input(
1308                    session_id,
1309                    durability_input,
1310                    "ResolveRuntimeOpsLifecycleDurability",
1311                )
1312                .await
1313                .map_err(|reason| RuntimeDriverError::ValidationFailed { reason })?;
1314            runtime_ops_lifecycle_durability_authority_from_effects(
1315                session_id,
1316                &durability_effects,
1317            )?
1318        } else {
1319            RuntimeOpsLifecycleDurabilityAuthority {
1320                action:
1321                    crate::meerkat_machine::dsl::RuntimeOpsLifecycleDurabilityAction::RetainSnapshot,
1322            }
1323        };
1324        let staged = self
1325            .stage_session_dsl_transition(session_id, unregister_input, "UnregisterSession")
1326            .await
1327            .map_err(|reason| RuntimeDriverError::ValidationFailed { reason })?;
1328        Ok((staged, authority))
1329    }
1330
1331    async fn finalize_unregistered_session(
1332        &self,
1333        runtime_id: LogicalRuntimeId,
1334        driver: SharedDriver,
1335        durability_authority: RuntimeOpsLifecycleDurabilityAuthority,
1336    ) -> Result<(), RuntimeDriverError> {
1337        let mut driver = driver.lock().await;
1338        driver.sync_control_projection_from_dsl_authority();
1339        drop(driver);
1340
1341        if durability_authority.action
1342            != crate::meerkat_machine::dsl::RuntimeOpsLifecycleDurabilityAction::DeleteSnapshot
1343        {
1344            return Ok(());
1345        }
1346        let Some(store) = self.store.as_ref() else {
1347            return Ok(());
1348        };
1349        store.delete_ops_lifecycle(&runtime_id).await.map_err(|err| {
1350            RuntimeDriverError::Internal(format!(
1351                "failed to delete ops lifecycle snapshot for unregistered runtime {runtime_id}: {err}"
1352            ))
1353        })?;
1354        Ok(())
1355    }
1356
1357    pub(super) async fn unregister_session_inner(&self, session_id: &SessionId) {
1358        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner start");
1359        let Some(gate_guard) = self.lock_current_session_mutation_gate(session_id).await else {
1360            tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner no mutation gate");
1361            return;
1362        };
1363        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner locked mutation gate");
1364        if let Err(err) =
1365            Box::pin(self.unregister_session_inner_locked_authorized(session_id, gate_guard)).await
1366        {
1367            tracing::warn!(
1368                %session_id,
1369                error = %err,
1370                "generated MeerkatMachine rejected session unregister"
1371            );
1372        }
1373        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner complete");
1374    }
1375
1376    /// Two-phase unregister drain (campaign 0.7.2 D1).
1377    ///
1378    /// The shell must quiesce every in-process producer of session-scoped
1379    /// inputs before the machine commits teardown, so a run that commits
1380    /// terminally while unregister races it still resolves its completion
1381    /// waiters with the committed outcome (never an authority error).
1382    ///
1383    /// Sequence:
1384    /// 1. (gate held) `BeginUnregisterSession` opens the machine-owned drain
1385    ///    window (`registration_phase = Draining`, three obligation flags set)
1386    ///    and emits the three `Request*ForUnregister` owner-realized effects.
1387    /// 2. Discharge the runtime-loop-stop obligation by detaching the loop
1388    ///    channels (dropping `wake_tx`/`effect_tx`) while keeping its
1389    ///    `JoinHandle`; discharge the comms-drain obligation by aborting the
1390    ///    drain task while keeping its `JoinHandle`.
1391    /// 3. **Drop the mutation gate.** The in-flight run commits and the loop
1392    ///    exits through `lock_current_runtime_loop_driver_authority`, which
1393    ///    re-acquires this same gate — awaiting the loop under the gate would
1394    ///    deadlock. The machine-owned `Draining` marker keeps the window safe:
1395    ///    `EnsureSessionWithExecutor` / `BeginUnregisterSession` re-entry are
1396    ///    guard-rejected, and the loop's own commits are exactly what we wait
1397    ///    for.
1398    /// 4. Await both `JoinHandle`s (the drain task's `JoinError::is_cancelled`
1399    ///    is benign — it was just aborted). No artificial timeout caps.
1400    /// 5. Re-acquire the gate; resolve any completion waiters the in-flight run
1401    ///    did not already resolve with the runtime-terminated outcome.
1402    /// 6. Fire the three `*ForUnregister` feedback inputs to close the
1403    ///    obligations.
1404    /// 7. Stage + commit the final `UnregisterSession`; persist, remove the
1405    ///    entry, finalize.
1406    pub(super) async fn unregister_session_inner_locked_authorized(
1407        &self,
1408        session_id: &SessionId,
1409        gate_guard: crate::tokio::sync::OwnedMutexGuard<()>,
1410    ) -> Result<(), RuntimeDriverError> {
1411        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized start");
1412        let driver_handle = {
1413            let sessions = self.sessions.read().await;
1414            sessions
1415                .get(session_id)
1416                .map(|entry| Arc::clone(&entry.driver))
1417                .ok_or(RuntimeDriverError::NotReady {
1418                    state: RuntimeState::Destroyed,
1419                })?
1420        };
1421
1422        // Phase 1: open the drain window. A concurrent second unregister whose
1423        // BeginUnregisterSession is rejected because the window is already open
1424        // is a benign already-in-progress observation, not an error. The
1425        // machine records whether teardown intent should retain the durable
1426        // runtime snapshot before the drain can advance lifecycle state.
1427        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized beginning drain window");
1428        let retry_final_unregister_only = match self
1429            .stage_begin_unregister_session_authority(session_id)
1430            .await
1431        {
1432            Ok(staged) => {
1433                self.commit_session_dsl_transition(session_id, staged, "BeginUnregisterSession")
1434                    .await
1435                    .map_err(RuntimeDriverError::Internal)?;
1436                false
1437            }
1438            Err(reason) => {
1439                let already_draining =
1440                    self.session_dsl_state(session_id).await.is_ok_and(|state| {
1441                        state.registration_phase
1442                            == crate::meerkat_machine::dsl::RegistrationPhase::Draining
1443                    });
1444                if already_draining {
1445                    tracing::debug!(
1446                        %session_id,
1447                        "BeginUnregisterSession rejected: drain already in progress; attempting final unregister retry only"
1448                    );
1449                    true
1450                } else {
1451                    return Err(self
1452                        .classify_session_dsl_rejection(session_id, reason)
1453                        .await);
1454                }
1455            }
1456        };
1457
1458        if retry_final_unregister_only {
1459            return self
1460                .retry_final_unregister_after_completed_drain(
1461                    session_id,
1462                    Arc::clone(&driver_handle),
1463                )
1464                .await;
1465        }
1466
1467        // Phase 2: discharge the runtime-loop-stop and comms-drain-abort
1468        // obligations, retaining both JoinHandles to await below. The live
1469        // interrupt handle is captured before `take_loop_join_handle` empties
1470        // the attachment slot, so the drain can hard-cancel an in-flight run
1471        // (see Phase 4).
1472        let (loop_handle, loop_interrupt_handle, drain_handle) = {
1473            let mut sessions = self.sessions.write().await;
1474            match sessions.get_mut(session_id) {
1475                Some(entry) => {
1476                    let interrupt_handle = entry.interrupt_handle();
1477                    (
1478                        entry.take_loop_join_handle(),
1479                        interrupt_handle,
1480                        entry.drain_slot.abort_keeping_handle(),
1481                    )
1482                }
1483                None => (None, None, None),
1484            }
1485        };
1486
1487        // Phase 3: drop the mutation gate so the in-flight run and the runtime
1488        // loop can re-acquire it to commit and exit. Phase 4: await quiescence.
1489        drop(gate_guard);
1490        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized awaiting runtime-loop and comms-drain quiescence");
1491        // Track whether each producer concluded cleanly or had to be
1492        // force-aborted after its drain grace window. The feedback inputs
1493        // below carry this disposition so the machine records a forced
1494        // teardown honestly instead of laundering it as clean quiescence.
1495        let mut runtime_loop_forced_abort = false;
1496        let mut comms_drain_forced_abort = false;
1497        if let Some(loop_handle) = loop_handle {
1498            // Dropping `wake_tx`/`effect_tx` (above) drives the loop through its
1499            // canonical `StopRuntimeExecutor` exit *once it returns to its
1500            // `select!`* — but a loop blocked inside `CoreExecutor::apply`
1501            // (mid `start_turn`) never observes the closed channel. Hard-cancel
1502            // the in-flight run so a well-behaved executor unwinds `apply` and
1503            // the loop reaches its clean exit (StopRuntimeExecutor +
1504            // discard_live_session) promptly.
1505            if let Some(interrupt_handle) = loop_interrupt_handle
1506                && let Err(error) = interrupt_handle
1507                    .hard_cancel_current_run("runtime session unregistered".to_string())
1508                    .await
1509            {
1510                tracing::debug!(
1511                    %session_id,
1512                    %error,
1513                    "in-flight run hard-cancel during unregister drain returned an error (benign if no run was active)"
1514                );
1515            }
1516
1517            // A backend that does not honor the interrupt (a genuinely stuck
1518            // turn) would otherwise wedge the loop's `JoinHandle` forever.
1519            // Give the loop a grace window to complete its clean exit, then
1520            // abort the task so teardown cannot stall on a stuck run. The
1521            // grace is far above any realistic clean-exit latency (sub-ms once
1522            // `apply` returns) and far below caller shutdown budgets, so the
1523            // responsive path never reaches the abort and its
1524            // StopRuntimeExecutor + discard_live_session cleanup is preserved.
1525            const RUNTIME_LOOP_DRAIN_GRACE: std::time::Duration = std::time::Duration::from_secs(2);
1526            let abort_handle = loop_handle.abort_handle();
1527            match crate::tokio::time::timeout(RUNTIME_LOOP_DRAIN_GRACE, loop_handle).await {
1528                Ok(Ok(())) => {}
1529                Ok(Err(join_error)) => {
1530                    tracing::warn!(
1531                        %session_id,
1532                        error = %join_error,
1533                        "runtime loop task ended abnormally during unregister drain"
1534                    );
1535                }
1536                Err(_elapsed) => {
1537                    abort_handle.abort();
1538                    runtime_loop_forced_abort = true;
1539                    tracing::warn!(
1540                        %session_id,
1541                        "runtime loop did not quiesce within the unregister drain grace window after hard-cancel; aborting the stuck loop task"
1542                    );
1543                }
1544            }
1545        }
1546        if let Some(drain_handle) = drain_handle {
1547            // The comms drain task was already aborted via
1548            // `abort_keeping_handle()` above; await its quiescence, but BOUND
1549            // the wait exactly like the runtime-loop handle. An external member
1550            // (e.g. a TCP transport drain) whose task is parked in an operation
1551            // that does not observe the cooperative abort promptly would
1552            // otherwise wedge teardown forever on an unbounded `.await`
1553            // (regression: `external_tcp_production_drain` hung past 900s). The
1554            // grace is far above any realistic cancel latency; on elapse we
1555            // abort the handle and proceed — the task is already aborted and
1556            // will unwind, and teardown must not stall on it.
1557            const COMMS_DRAIN_GRACE: std::time::Duration = std::time::Duration::from_secs(2);
1558            let drain_abort = drain_handle.abort_handle();
1559            match crate::tokio::time::timeout(COMMS_DRAIN_GRACE, drain_handle).await {
1560                Ok(Ok(())) => {}
1561                Ok(Err(join_error)) if join_error.is_cancelled() => {}
1562                Ok(Err(join_error)) => {
1563                    tracing::warn!(
1564                        %session_id,
1565                        error = %join_error,
1566                        "comms drain task ended abnormally during unregister drain"
1567                    );
1568                }
1569                Err(_elapsed) => {
1570                    drain_abort.abort();
1571                    comms_drain_forced_abort = true;
1572                    tracing::warn!(
1573                        %session_id,
1574                        "comms drain task did not quiesce within the unregister drain grace window; abandoning the already-aborted drain task so teardown cannot stall"
1575                    );
1576                }
1577            }
1578        }
1579
1580        // Phase 5: re-acquire the gate. If the session vanished while the gate
1581        // was released (e.g. a racing teardown), the drain already completed
1582        // elsewhere — nothing left to commit.
1583        let Some(_gate_guard) = self.lock_current_session_mutation_gate(session_id).await else {
1584            tracing::debug!(
1585                %session_id,
1586                "session removed by a concurrent teardown during unregister drain (benign)"
1587            );
1588            return Ok(());
1589        };
1590        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized re-acquired mutation gate after drain");
1591
1592        // Resolve any completion waiters the in-flight run did not already
1593        // resolve. A run that committed during the drain window resolves its
1594        // own waiter with the committed outcome; this sweep terminalizes any
1595        // that are still outstanding so the final commit cannot strand them.
1596        let runtime_terminated_completion_authority =
1597            crate::meerkat_machine::driver::machine_resolve_runtime_terminated_completion_result(
1598                &driver_handle,
1599            )
1600            .await?;
1601        {
1602            let completions = {
1603                let sessions = self.sessions.read().await;
1604                sessions
1605                    .get(session_id)
1606                    .map(|entry| Arc::clone(&entry.completions))
1607            };
1608            if let Some(completions) = completions {
1609                // The drain-phase sweep is the single token-consuming
1610                // terminalization point for waiters the in-flight run did not
1611                // already resolve with its committed outcome.
1612                completions.lock().await.resolve_all_runtime_terminated(
1613                    "runtime session unregistered",
1614                    runtime_terminated_completion_authority,
1615                );
1616            }
1617        }
1618
1619        // Phase 6: fire the three feedback inputs to close the obligations.
1620        // Each runtime-loop / comms-drain input carries whether that producer
1621        // quiesced cleanly or had to be force-aborted after its grace window,
1622        // so the machine records the real teardown disposition.
1623        for (input, context) in [
1624            (
1625                crate::meerkat_machine::dsl::MeerkatMachineInput::RuntimeLoopStoppedForUnregister {
1626                    session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
1627                    forced_abort: runtime_loop_forced_abort,
1628                },
1629                "RuntimeLoopStoppedForUnregister",
1630            ),
1631            (
1632                crate::meerkat_machine::dsl::MeerkatMachineInput::CommsDrainExitedForUnregister {
1633                    session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
1634                    forced_abort: comms_drain_forced_abort,
1635                },
1636                "CommsDrainExitedForUnregister",
1637            ),
1638            (
1639                crate::meerkat_machine::dsl::MeerkatMachineInput::CompletionWaitersResolvedForUnregister {
1640                    session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
1641                },
1642                "CompletionWaitersResolvedForUnregister",
1643            ),
1644        ] {
1645            let staged = match self
1646                .stage_session_dsl_transition(session_id, input, context)
1647                .await
1648            {
1649                Ok(staged) => staged,
1650                Err(reason) => return Err(RuntimeDriverError::ValidationFailed { reason }),
1651            };
1652            self.commit_session_dsl_transition(session_id, staged, context)
1653                .await
1654                .map_err(RuntimeDriverError::Internal)?;
1655        }
1656
1657        // Phase 7: stage + commit the final UnregisterSession.
1658        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized staging unregister");
1659        let (staged, durability_authority) =
1660            self.stage_unregister_session_authority(session_id).await?;
1661        let unregister_rollback_snapshot = staged.previous_snapshot.clone();
1662        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized committing unregister");
1663        self.commit_session_dsl_transition(session_id, staged, "UnregisterSession")
1664            .await
1665            .map_err(RuntimeDriverError::Internal)?;
1666        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized committed unregister");
1667        if durability_authority.action
1668            == crate::meerkat_machine::dsl::RuntimeOpsLifecycleDurabilityAction::DeleteSnapshot
1669        {
1670            driver_handle
1671                .lock()
1672                .await
1673                .persist_current_machine_lifecycle("unregister")
1674                .await?;
1675        }
1676        let finalize_target = {
1677            let sessions = self.sessions.read().await;
1678            sessions
1679                .get(session_id)
1680                .map(|entry| (entry.runtime_id.clone(), Arc::clone(&entry.driver)))
1681        };
1682        if let Some((runtime_id, driver)) = finalize_target {
1683            tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized finalizing durable unregister");
1684            if let Err(err) = self
1685                .finalize_unregistered_session(
1686                    runtime_id,
1687                    Arc::clone(&driver),
1688                    durability_authority,
1689                )
1690                .await
1691            {
1692                self.restore_session_dsl_state(session_id, unregister_rollback_snapshot)
1693                    .await;
1694                driver
1695                    .lock()
1696                    .await
1697                    .sync_control_projection_from_dsl_authority();
1698                if let Err(rollback_error) = driver
1699                    .lock()
1700                    .await
1701                    .persist_current_machine_lifecycle("unregister rollback")
1702                    .await
1703                {
1704                    return Err(RuntimeDriverError::Internal(format!(
1705                        "{err}; additionally failed to persist unregister rollback: {rollback_error}"
1706                    )));
1707                }
1708                return Err(err);
1709            }
1710        }
1711        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized removing entry");
1712        let entry = {
1713            let mut sessions = self.sessions.write().await;
1714            // Abort the drain slot inline before removing the entry — the
1715            // slot is now owned by the entry itself (wave-c C-H2), so the
1716            // "slot keys are a subset of registered-session keys" invariant
1717            // is structural rather than enforced by ordering.
1718            if let Some(entry) = sessions.get_mut(session_id) {
1719                entry.close_handle_teardown_gate();
1720                abort_slot(&mut entry.drain_slot);
1721            }
1722            sessions.remove(session_id)
1723        };
1724        drop(entry);
1725        tracing::info!(%session_id, "MeerkatMachine::unregister_session_inner_locked_authorized complete");
1726        Ok(())
1727    }
1728
1729    async fn retry_final_unregister_after_completed_drain(
1730        &self,
1731        session_id: &SessionId,
1732        driver_handle: SharedDriver,
1733    ) -> Result<(), RuntimeDriverError> {
1734        let (staged, durability_authority) =
1735            match self.stage_unregister_session_authority(session_id).await {
1736                Ok(pair) => pair,
1737                Err(err @ RuntimeDriverError::ValidationFailed { .. }) => {
1738                    tracing::debug!(
1739                        %session_id,
1740                        error = %err,
1741                        "final unregister retry is not ready; original drain remains in progress"
1742                    );
1743                    return Ok(());
1744                }
1745                Err(err) => return Err(err),
1746            };
1747        let unregister_rollback_snapshot = staged.previous_snapshot.clone();
1748        self.commit_session_dsl_transition(session_id, staged, "UnregisterSession")
1749            .await
1750            .map_err(RuntimeDriverError::Internal)?;
1751        if durability_authority.action
1752            == crate::meerkat_machine::dsl::RuntimeOpsLifecycleDurabilityAction::DeleteSnapshot
1753        {
1754            driver_handle
1755                .lock()
1756                .await
1757                .persist_current_machine_lifecycle("unregister")
1758                .await?;
1759        }
1760        let finalize_target = {
1761            let sessions = self.sessions.read().await;
1762            sessions
1763                .get(session_id)
1764                .map(|entry| (entry.runtime_id.clone(), Arc::clone(&entry.driver)))
1765        };
1766        if let Some((runtime_id, driver)) = finalize_target
1767            && let Err(err) = self
1768                .finalize_unregistered_session(
1769                    runtime_id,
1770                    Arc::clone(&driver),
1771                    durability_authority,
1772                )
1773                .await
1774        {
1775            self.restore_session_dsl_state(session_id, unregister_rollback_snapshot)
1776                .await;
1777            driver
1778                .lock()
1779                .await
1780                .sync_control_projection_from_dsl_authority();
1781            if let Err(rollback_error) = driver
1782                .lock()
1783                .await
1784                .persist_current_machine_lifecycle("unregister rollback")
1785                .await
1786            {
1787                return Err(RuntimeDriverError::Internal(format!(
1788                    "{err}; additionally failed to persist unregister rollback: {rollback_error}"
1789                )));
1790            }
1791            return Err(err);
1792        }
1793        let entry = {
1794            let mut sessions = self.sessions.write().await;
1795            if let Some(entry) = sessions.get_mut(session_id) {
1796                entry.close_handle_teardown_gate();
1797                abort_slot(&mut entry.drain_slot);
1798            }
1799            sessions.remove(session_id)
1800        };
1801        drop(entry);
1802        Ok(())
1803    }
1804
1805    /// Check whether a runtime driver is already registered for a session.
1806    pub async fn contains_session(&self, session_id: &SessionId) -> bool {
1807        self.sessions.read().await.contains_key(session_id)
1808    }
1809
1810    /// Drop an in-memory, storeless WASM session entry after generated runtime
1811    /// authority has already terminalized it.
1812    #[cfg(target_arch = "wasm32")]
1813    pub async fn discard_terminal_storeless_session(&self, session_id: &SessionId) -> bool {
1814        if self.store.is_some() {
1815            return false;
1816        }
1817        let Some(snapshot) = self.meerkat_machine_archive_snapshot(session_id).await else {
1818            return false;
1819        };
1820        if !matches!(
1821            snapshot.control.phase,
1822            RuntimeState::Retired | RuntimeState::Stopped
1823        ) || !snapshot.queue.is_empty()
1824            || !snapshot.steer_queue.is_empty()
1825        {
1826            return false;
1827        }
1828        let Some(_gate_guard) = self.lock_current_session_mutation_gate(session_id).await else {
1829            return false;
1830        };
1831        let (driver_handle, completions) = {
1832            let sessions = self.sessions.read().await;
1833            let Some(entry) = sessions.get(session_id) else {
1834                return false;
1835            };
1836            (Arc::clone(&entry.driver), Arc::clone(&entry.completions))
1837        };
1838        let runtime_terminated_completion_authority =
1839            match crate::meerkat_machine::driver::machine_resolve_runtime_terminated_completion_result(
1840                &driver_handle,
1841            )
1842            .await
1843            {
1844                Ok(authority) => authority,
1845                Err(err) => {
1846                    tracing::warn!(
1847                        %session_id,
1848                        error = %err,
1849                        "failed to resolve terminal completion authority for storeless WASM session discard"
1850                    );
1851                    return false;
1852                }
1853            };
1854        completions.lock().await.resolve_all_runtime_terminated(
1855            "storeless WASM session discarded",
1856            runtime_terminated_completion_authority,
1857        );
1858
1859        // The terminal storeless session has no attached runtime loop or comms
1860        // drain task to quiesce, so the drain obligations are discharged
1861        // trivially: open the window (Begin) then immediately close all three
1862        // obligations before committing the final UnregisterSession. This keeps
1863        // the wasm discard path on the same machine-owned teardown contract as
1864        // the native unregister drain.
1865        match self
1866            .stage_begin_unregister_session_authority(session_id)
1867            .await
1868        {
1869            Ok(staged) => {
1870                if let Err(err) = self
1871                    .commit_session_dsl_transition(session_id, staged, "BeginUnregisterSession")
1872                    .await
1873                {
1874                    tracing::warn!(
1875                        %session_id,
1876                        error = %err,
1877                        "failed to open drain window for storeless WASM session discard"
1878                    );
1879                    return false;
1880                }
1881            }
1882            Err(reason) => {
1883                tracing::warn!(
1884                    %session_id,
1885                    error = %reason,
1886                    "generated MeerkatMachine rejected drain-window open for storeless WASM session discard"
1887                );
1888                return false;
1889            }
1890        }
1891        // A terminal storeless session has no runtime loop or comms drain task
1892        // attached, so both producers conclude trivially (cleanly) — there is
1893        // nothing to force-abort.
1894        for (input, context) in [
1895            (
1896                crate::meerkat_machine::dsl::MeerkatMachineInput::RuntimeLoopStoppedForUnregister {
1897                    session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
1898                    forced_abort: false,
1899                },
1900                "RuntimeLoopStoppedForUnregister",
1901            ),
1902            (
1903                crate::meerkat_machine::dsl::MeerkatMachineInput::CommsDrainExitedForUnregister {
1904                    session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
1905                    forced_abort: false,
1906                },
1907                "CommsDrainExitedForUnregister",
1908            ),
1909            (
1910                crate::meerkat_machine::dsl::MeerkatMachineInput::CompletionWaitersResolvedForUnregister {
1911                    session_id: crate::meerkat_machine::dsl::SessionId::from_domain(session_id),
1912                },
1913                "CompletionWaitersResolvedForUnregister",
1914            ),
1915        ] {
1916            match self
1917                .stage_session_dsl_transition(session_id, input, context)
1918                .await
1919            {
1920                Ok(staged) => {
1921                    if let Err(err) = self
1922                        .commit_session_dsl_transition(session_id, staged, context)
1923                        .await
1924                    {
1925                        tracing::warn!(
1926                            %session_id,
1927                            error = %err,
1928                            "failed to close drain obligation for storeless WASM session discard"
1929                        );
1930                        return false;
1931                    }
1932                }
1933                Err(reason) => {
1934                    tracing::warn!(
1935                        %session_id,
1936                        error = %reason,
1937                        "generated MeerkatMachine rejected drain feedback for storeless WASM session discard"
1938                    );
1939                    return false;
1940                }
1941            }
1942        }
1943        let (staged, _durability) = match self.stage_unregister_session_authority(session_id).await
1944        {
1945            Ok(pair) => pair,
1946            Err(err) => {
1947                tracing::warn!(
1948                    %session_id,
1949                    error = %err,
1950                    "failed to stage final unregister for storeless WASM session discard"
1951                );
1952                return false;
1953            }
1954        };
1955        if let Err(err) = self
1956            .commit_session_dsl_transition(session_id, staged, "UnregisterSession")
1957            .await
1958        {
1959            tracing::warn!(
1960                %session_id,
1961                error = %err,
1962                "failed to commit final unregister for storeless WASM session discard"
1963            );
1964            return false;
1965        }
1966
1967        let entry = {
1968            let mut sessions = self.sessions.write().await;
1969            if let Some(entry) = sessions.get_mut(session_id) {
1970                abort_slot(&mut entry.drain_slot);
1971            }
1972            sessions.remove(session_id)
1973        };
1974        let Some(entry) = entry else {
1975            return false;
1976        };
1977        let runtime_id = entry.runtime_id.clone();
1978        let driver = Arc::clone(&entry.driver);
1979        if let Err(err) = self
1980            .finalize_unregistered_session(
1981                runtime_id,
1982                driver,
1983            RuntimeOpsLifecycleDurabilityAuthority {
1984                action:
1985                    crate::meerkat_machine::dsl::RuntimeOpsLifecycleDurabilityAction::RetainSnapshot,
1986            },
1987        )
1988            .await
1989        {
1990            tracing::warn!(
1991                %session_id,
1992                error = %err,
1993                "failed to finalize storeless WASM session discard"
1994            );
1995            return false;
1996        }
1997        true
1998    }
1999
2000    /// Check whether a session has an active RuntimeLoop or attachment in
2001    /// progress.
2002    ///
2003    /// `Ok(false)` means only `Queuing` (registered via `prepare_bindings()`
2004    /// with no executor) or unknown. Driver faults are returned explicitly so
2005    /// callers cannot accidentally treat a control-plane fault as absence.
2006    pub async fn session_has_executor(
2007        &self,
2008        session_id: &SessionId,
2009    ) -> Result<bool, RuntimeDriverError> {
2010        match self
2011            .execute_meerkat_machine_command(
2012                None,
2013                MeerkatMachineCommand::SessionHasExecutor {
2014                    session_id: session_id.clone(),
2015                },
2016            )
2017            .await
2018        {
2019            Ok(MeerkatMachineCommandResult::Bool(present)) => Ok(present),
2020            Ok(other) => Err(RuntimeDriverError::Internal(format!(
2021                "session_has_executor: unexpected command result variant: {other:?}"
2022            ))),
2023            Err(error) => Err(MeerkatMachine::driver_error_from_command_error(error)),
2024        }
2025    }
2026
2027    /// Wake the attached runtime loop when machine-owned input truth already
2028    /// contains active work. This does not mutate lifecycle state; it only
2029    /// replays the mechanical wake effect for callers that observe queued work
2030    /// at a boundary where user input must wait for canonical runtime work to
2031    /// drain.
2032    pub async fn wake_runtime_if_active_inputs(
2033        &self,
2034        session_id: &SessionId,
2035    ) -> Result<bool, RuntimeDriverError> {
2036        let (driver, wake_tx) = {
2037            let sessions = self.sessions.read().await;
2038            let entry = sessions
2039                .get(session_id)
2040                .ok_or(RuntimeDriverError::NotReady {
2041                    state: RuntimeState::Destroyed,
2042                })?;
2043            (entry.driver.clone(), entry.wake_sender())
2044        };
2045
2046        let has_active_inputs = {
2047            let driver = driver.lock().await;
2048            !driver.as_driver().active_input_ids().is_empty()
2049        };
2050        if !has_active_inputs {
2051            return Ok(false);
2052        }
2053
2054        let Some(wake_tx) = wake_tx else {
2055            return Err(RuntimeDriverError::NotReady {
2056                state: RuntimeState::Idle,
2057            });
2058        };
2059
2060        match wake_tx.try_send(()) {
2061            Ok(()) | Err(mpsc::error::TrySendError::Full(())) => Ok(true),
2062            Err(mpsc::error::TrySendError::Closed(())) => Err(RuntimeDriverError::NotReady {
2063                state: RuntimeState::Idle,
2064            }),
2065        }
2066    }
2067
2068    /// Check whether a session already has a comms runtime configured.
2069    ///
2070    /// Returns `true` if `update_peer_ingress_context` was previously called
2071    /// with a non-None comms runtime for this session (e.g., via
2072    /// `SessionRuntime::enable_comms_drain`).
2073    pub async fn session_has_comms(
2074        &self,
2075        session_id: &SessionId,
2076    ) -> Result<bool, RuntimeDriverError> {
2077        match self
2078            .execute_meerkat_machine_command(
2079                None,
2080                MeerkatMachineCommand::SessionHasComms {
2081                    session_id: session_id.clone(),
2082                },
2083            )
2084            .await
2085        {
2086            Ok(MeerkatMachineCommandResult::Bool(present)) => Ok(present),
2087            Ok(other) => Err(RuntimeDriverError::Internal(format!(
2088                "session_has_comms: unexpected command result variant: {other:?}"
2089            ))),
2090            Err(error) => Err(MeerkatMachine::driver_error_from_command_error(error)),
2091        }
2092    }
2093
2094    /// Resolve the session-liveness verdict for an attempted transcript edit
2095    /// (fork / rewrite / restore) through MeerkatMachine authority.
2096    ///
2097    /// The `SESSION_BUSY` disjunction (`runtime_running || has_active_inputs =>
2098    /// busy`) is a MeerkatMachine-owned fact. The shell extracts the two pure
2099    /// boolean observations it already computes — `runtime_running` from
2100    /// `runtime_state` and `has_active_inputs` from `list_active_inputs` — and
2101    /// mirrors the verdict emitted here. The classifier is a phase-preserving
2102    /// self-loop, so it never mutates lifecycle state. The caller fails closed
2103    /// (denies the edit) on any error.
2104    pub async fn resolve_transcript_edit_admission(
2105        &self,
2106        session_id: &SessionId,
2107        runtime_running: bool,
2108        has_active_inputs: bool,
2109    ) -> Result<crate::meerkat_machine::dsl::TranscriptEditAdmissionKind, RuntimeDriverError> {
2110        let (_, effects) = self
2111            .apply_session_dsl_input(
2112                session_id,
2113                crate::meerkat_machine::dsl::MeerkatMachineInput::ResolveTranscriptEditAdmission {
2114                    runtime_running,
2115                    has_active_inputs,
2116                },
2117                "ResolveTranscriptEditAdmission",
2118            )
2119            .await
2120            .map_err(RuntimeDriverError::Internal)?;
2121        effects
2122            .as_slice()
2123            .iter()
2124            .find_map(|effect| {
2125                match effect {
2126                crate::meerkat_machine::dsl::MeerkatMachineEffect::TranscriptEditAdmissionResolved {
2127                    verdict,
2128                } => Some(*verdict),
2129                _ => None,
2130            }
2131            })
2132            .ok_or_else(|| {
2133                RuntimeDriverError::Internal(
2134                    "transcript-edit admission emitted no authority verdict".to_string(),
2135                )
2136            })
2137    }
2138
2139    /// Request cancellation at the next safe boundary for the currently-running turn.
2140    pub async fn cancel_after_boundary(
2141        &self,
2142        session_id: &SessionId,
2143    ) -> Result<(), RuntimeDriverError> {
2144        self.execute_meerkat_machine_command(
2145            None,
2146            MeerkatMachineCommand::CancelAfterBoundary {
2147                session_id: session_id.clone(),
2148            },
2149        )
2150        .await
2151        .map_err(MeerkatMachine::driver_error_from_command_error)
2152        .map(|_| ())
2153    }
2154
2155    /// Realize pending-input abandonment after the machine has already entered
2156    /// the Retired terminal phase.
2157    pub async fn abandon_retired_pending_inputs(
2158        &self,
2159        session_id: &SessionId,
2160        reason: impl Into<String>,
2161    ) -> Result<usize, RuntimeDriverError> {
2162        let reason = reason.into();
2163        let state = self
2164            .existing_session_runtime_state(session_id)
2165            .await
2166            .unwrap_or(RuntimeState::Destroyed);
2167        if state != RuntimeState::Retired {
2168            return Err(RuntimeDriverError::NotReady { state });
2169        }
2170
2171        let gate = self.session_mutation_gate(session_id).await;
2172        let _gate_guard = match gate {
2173            Some(ref g) => Some(g.lock().await),
2174            None => None,
2175        };
2176
2177        let (driver, completions) = {
2178            let sessions = self.sessions.read().await;
2179            let entry = sessions
2180                .get(session_id)
2181                .ok_or(RuntimeDriverError::NotReady {
2182                    state: RuntimeState::Destroyed,
2183                })?;
2184            (entry.driver.clone(), entry.completions.clone())
2185        };
2186
2187        let abandoned = {
2188            let mut driver = driver.lock().await;
2189            driver
2190                .abandon_pending_inputs(crate::input_state::InputAbandonReason::Retired)
2191                .await?
2192        };
2193        let result_class =
2194            crate::meerkat_machine::driver::machine_resolve_runtime_terminated_completion_result(
2195                &driver,
2196            )
2197            .await?;
2198        completions
2199            .lock()
2200            .await
2201            .resolve_all_runtime_terminated(&reason, result_class);
2202        Ok(abandoned)
2203    }
2204
2205    /// Stage a durable session visibility filter through the machine-owned visibility state.
2206    pub async fn stage_persistent_filter(
2207        &self,
2208        session_id: &SessionId,
2209        filter: meerkat_core::ToolFilter,
2210        witnesses: std::collections::BTreeMap<
2211            meerkat_core::ToolName,
2212            meerkat_core::ToolVisibilityWitness,
2213        >,
2214    ) -> Result<meerkat_core::ToolScopeRevision, RuntimeDriverError> {
2215        match self
2216            .execute_meerkat_machine_command(
2217                None,
2218                MeerkatMachineCommand::StagePersistentFilter {
2219                    session_id: session_id.clone(),
2220                    filter,
2221                    witnesses,
2222                },
2223            )
2224            .await
2225            .map_err(MeerkatMachine::driver_error_from_command_error)?
2226        {
2227            MeerkatMachineCommandResult::VisibilityRevision(revision) => Ok(revision),
2228            other => Err(RuntimeDriverError::Internal(format!(
2229                "unexpected MeerkatMachineCommandResult for stage_persistent_filter: {other:?}"
2230            ))),
2231        }
2232    }
2233
2234    /// Record durable deferred-tool visibility intent through the machine seam.
2235    pub async fn request_deferred_tools(
2236        &self,
2237        session_id: &SessionId,
2238        authorities: Vec<meerkat_core::DeferredToolLoadAuthority>,
2239    ) -> Result<meerkat_core::ToolScopeRevision, RuntimeDriverError> {
2240        match self
2241            .execute_meerkat_machine_command(
2242                None,
2243                MeerkatMachineCommand::RequestDeferredTools {
2244                    session_id: session_id.clone(),
2245                    authorities,
2246                },
2247            )
2248            .await
2249            .map_err(MeerkatMachine::driver_error_from_command_error)?
2250        {
2251            MeerkatMachineCommandResult::VisibilityRevision(revision) => Ok(revision),
2252            other => Err(RuntimeDriverError::Internal(format!(
2253                "unexpected MeerkatMachineCommandResult for request_deferred_tools: {other:?}"
2254            ))),
2255        }
2256    }
2257
2258    /// Publish the committed visible tool set through the machine dispatch.
2259    ///
2260    /// Routes the visibility publication through the canonical command path,
2261    /// enforcing session-existence and Destroyed guards per the TLA+
2262    /// `VisibleSurfacesMatchAppliedStateInvariant`.
2263    ///
2264    /// Returns the validated visibility state on success.
2265    pub async fn publish_committed_visible_set(
2266        &self,
2267        session_id: &SessionId,
2268        visibility_state: meerkat_core::SessionToolVisibilityState,
2269    ) -> Result<meerkat_core::SessionToolVisibilityState, RuntimeDriverError> {
2270        match self
2271            .execute_meerkat_machine_command(
2272                None,
2273                MeerkatMachineCommand::PublishCommittedVisibleSet {
2274                    session_id: session_id.clone(),
2275                    visibility_state: Box::new(visibility_state),
2276                },
2277            )
2278            .await
2279            .map_err(MeerkatMachine::driver_error_from_command_error)?
2280        {
2281            MeerkatMachineCommandResult::VisibilityPublished(state) => Ok(state),
2282            other => Err(RuntimeDriverError::Internal(format!(
2283                "unexpected MeerkatMachineCommandResult for publish_committed_visible_set: {other:?}"
2284            ))),
2285        }
2286    }
2287
2288    /// Install the runtime-owned shell seam for live LLM reconfiguration.
2289    pub fn set_session_llm_reconfigure_host(&self, host: Arc<dyn SessionLlmReconfigureHost>) {
2290        *self
2291            .llm_reconfigure_host
2292            .write()
2293            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(host);
2294    }
2295
2296    // NOTE: Realtime-attachment public API was removed as part of
2297    // the realtime/live-topology DSL plane deletion.
2298    // Provider session lifecycle now lives outside MeerkatMachine (live-adapter MVP).
2299}