Skip to main content

meerkat_runtime/
completion.rs

1//! Input completion waiters — allows callers to await terminal outcome of an accepted input.
2//!
3//! When a surface accepts an input via the runtime, it can optionally receive a
4//! `CompletionHandle` that resolves when the input reaches a terminal state
5//! (Consumed or Abandoned). This bridges the async accept/await pattern needed
6//! for surfaces that want synchronous-feeling turn execution through the runtime.
7//!
8//! `CompletionRegistry` is waiter plumbing only. Production code must never
9//! treat waiter presence, waiter counts, or sender membership as semantic
10//! runtime truth.
11
12use std::collections::HashMap;
13use std::future::Future;
14
15use meerkat_core::lifecycle::InputId;
16#[cfg(test)]
17use meerkat_core::lifecycle::RunId;
18use meerkat_core::lifecycle::core_executor::CoreApplyTerminal;
19use meerkat_core::types::{RunResult, SessionId};
20use meerkat_core::{TurnErrorMetadata, TurnTerminalCauseKind, TurnTerminalOutcome};
21use serde_json::Value;
22
23use crate::meerkat_machine::driver::{
24    RuntimeCompletionResultAttempt, RuntimeCompletionResultAuthority,
25    RuntimeCompletionResultRealized,
26};
27use crate::meerkat_machine::dsl::RuntimeCompletionResultClass;
28use crate::tokio::sync::oneshot;
29
30/// Mechanical failure while waiting for completion plumbing.
31///
32/// This is intentionally separate from [`CompletionOutcome`]: a closed waiter
33/// channel or missing generated completion authority is not a public runtime
34/// result class.
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum CompletionWaitError {
37    #[error("completion channel closed without an authorized result")]
38    ChannelClosed,
39    #[error("{0}")]
40    AuthorityUnavailable(String),
41}
42
43impl CompletionWaitError {
44    pub fn wait_failure_observation(
45        &self,
46    ) -> crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation {
47        match self {
48            Self::ChannelClosed => {
49                crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation::ChannelClosed
50            }
51            Self::AuthorityUnavailable(_) => {
52                crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation::AuthorityUnavailable
53            }
54        }
55    }
56}
57
58/// Outcome delivered to a completion waiter.
59#[derive(Debug)]
60pub enum CompletionOutcome {
61    /// The input was successfully consumed and produced a result.
62    Completed(Box<RunResult>),
63    /// The input was consumed but produced no RunResult (e.g. context-append ops).
64    CompletedWithoutResult,
65    /// The input reached a callback boundary and requires external tool
66    /// fulfillment before the turn can continue.
67    CallbackPending { tool_name: String, args: Value },
68    /// The input reached the canonical cancellation terminal.
69    Cancelled,
70    /// The input was abandoned before completing, carrying typed failure
71    /// metadata so every surface sees the same structured turn error the
72    /// sibling [`AbandonedWithError`](Self::AbandonedWithError) carries.
73    Abandoned {
74        reason: String,
75        error: TurnErrorMetadata,
76    },
77    /// The input was abandoned before completing, with typed failure metadata.
78    AbandonedWithError {
79        reason: String,
80        error: TurnErrorMetadata,
81    },
82    /// The turn produced output, but a later runtime finalization step (the
83    /// durable commit) failed, so the run is NOT durably terminal. The produced
84    /// result is deliberately NOT carried on this outcome: a finalization
85    /// failure must be treated as failure by every surface, never surfaced as a
86    /// usable success result (that would be a false belief of success).
87    CompletedWithFinalizationFailure { error: TurnErrorMetadata },
88    /// The runtime was stopped or destroyed while the input was pending,
89    /// carrying typed failure metadata describing the termination cause.
90    RuntimeTerminated {
91        reason: String,
92        error: TurnErrorMetadata,
93    },
94}
95
96/// Runtime-minted observation for post-completion cleanup.
97///
98/// Cleanup code can inspect this generated-facing observation, but it cannot
99/// rewrite the public [`CompletionOutcome`] that the waiter received.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct CompletionCleanupObservation {
102    owner_session_id: SessionId,
103    owner_agent_runtime_id: Option<crate::meerkat_machine::dsl::AgentRuntimeId>,
104    owner_fence_token: Option<crate::meerkat_machine::dsl::FenceToken>,
105    owner_runtime_generation: Option<crate::meerkat_machine::dsl::Generation>,
106    owner_runtime_epoch_id: Option<crate::meerkat_machine::dsl::RuntimeEpochId>,
107    observed_outcome: crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome,
108}
109
110impl CompletionCleanupObservation {
111    fn from_realized_result(realized: RuntimeCompletionResultRealized) -> Self {
112        Self {
113            owner_session_id: realized.session_id().clone(),
114            owner_agent_runtime_id: realized.agent_runtime_id().cloned(),
115            owner_fence_token: realized.fence_token(),
116            owner_runtime_generation: realized.runtime_generation(),
117            owner_runtime_epoch_id: realized.runtime_epoch_id().cloned(),
118            observed_outcome: realized.cleanup_observation(),
119        }
120    }
121
122    /// Return whether this runtime-minted observation proves that `session_id`
123    /// reached the machine-owned runtime-termination completion class.
124    ///
125    /// Cleanup relays use this narrow proof when the machine-owned unregister
126    /// saga wins the race and removes the runtime registration after the
127    /// executor's external-only cleanup, before the completion relay inspects
128    /// the registry. It must not be replaced with a generic "registration is
129    /// absent" check: absence alone carries no terminal authority.
130    #[must_use]
131    pub fn proves_runtime_termination_for(&self, session_id: &SessionId) -> bool {
132        self.owner_session_id == *session_id
133            && self.observed_outcome
134                == crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome::RuntimeTerminated
135    }
136
137    pub(crate) fn owner_session_id(&self) -> &SessionId {
138        &self.owner_session_id
139    }
140
141    pub(crate) fn owner_agent_runtime_id(
142        &self,
143    ) -> Option<&crate::meerkat_machine::dsl::AgentRuntimeId> {
144        self.owner_agent_runtime_id.as_ref()
145    }
146
147    pub(crate) fn owner_fence_token(&self) -> Option<crate::meerkat_machine::dsl::FenceToken> {
148        self.owner_fence_token
149    }
150
151    pub(crate) fn owner_runtime_generation(
152        &self,
153    ) -> Option<crate::meerkat_machine::dsl::Generation> {
154        self.owner_runtime_generation
155    }
156
157    pub(crate) fn owner_runtime_epoch_id(
158        &self,
159    ) -> Option<&crate::meerkat_machine::dsl::RuntimeEpochId> {
160        self.owner_runtime_epoch_id.as_ref()
161    }
162
163    pub(crate) fn observed_outcome(
164        &self,
165    ) -> crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome {
166        self.observed_outcome
167    }
168}
169
170/// Result carried on completion waiter plumbing after generated authority has
171/// selected both the public result class and cleanup observation.
172#[derive(Debug)]
173struct CompletionDelivery {
174    outcome: CompletionOutcome,
175    cleanup_observation: CompletionCleanupObservation,
176}
177
178/// Snapshot of one input's registered completion waiters.
179///
180/// This is a diagnostic/supporting-carrier view only. Waiter counts are never
181/// semantic runtime truth.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct CompletionWaiterEntrySnapshot {
184    pub input_id: InputId,
185    pub waiter_count: usize,
186}
187
188/// Diagnostic snapshot of the completion waiter registry.
189///
190/// This makes the carrier explicit for MeerkatMachine mapping work without
191/// promoting waiter plumbing into canonical runtime semantics.
192#[derive(Debug, Clone, PartialEq, Eq, Default)]
193pub struct CompletionRegistrySnapshot {
194    pub input_count: usize,
195    pub waiter_count: usize,
196    pub waiting_inputs: Vec<CompletionWaiterEntrySnapshot>,
197}
198
199/// Handle for awaiting the completion of an accepted input.
200#[derive(Debug)]
201pub struct CompletionHandle {
202    rx: oneshot::Receiver<Result<CompletionDelivery, CompletionWaitError>>,
203}
204
205impl CompletionHandle {
206    async fn try_wait_delivery(self) -> Result<CompletionDelivery, CompletionWaitError> {
207        self.rx
208            .await
209            .unwrap_or(Err(CompletionWaitError::ChannelClosed))
210    }
211
212    /// Wait for the input to reach a terminal state or report mechanical waiter failure.
213    pub async fn try_wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
214        self.try_wait_delivery()
215            .await
216            .map(|delivery| delivery.outcome)
217    }
218
219    /// Wait for completion and return the generated cleanup observation carried
220    /// with the authorized public outcome.
221    pub async fn try_wait_with_cleanup_observation(
222        self,
223    ) -> Result<(CompletionOutcome, CompletionCleanupObservation), CompletionWaitError> {
224        let delivery = self.try_wait_delivery().await?;
225        Ok((delivery.outcome, delivery.cleanup_observation))
226    }
227
228    /// Wait for the input to reach a terminal state or report mechanical waiter failure.
229    pub async fn wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
230        self.try_wait().await
231    }
232
233    /// Wait for a test handle that is expected to resolve through generated authority.
234    #[cfg(test)]
235    pub(crate) async fn wait_authorized(self) -> CompletionOutcome {
236        self.wait()
237            .await
238            .expect("completion waiter closed without an authorized result")
239    }
240
241    /// Relay completion through a cleanup future before resolving the returned
242    /// handle. This lets surfaces transfer cleanup ownership immediately after
243    /// accepting runtime work while still returning a completion handle.
244    pub fn with_cleanup<F, Fut>(self, cleanup: F) -> Self
245    where
246        F: FnOnce() -> Fut + Send + 'static,
247        Fut: Future<Output = ()> + Send + 'static,
248    {
249        let (tx, rx) = oneshot::channel();
250        crate::tokio::spawn(async move {
251            let outcome = self.try_wait_delivery().await;
252            cleanup().await;
253            let _ = tx.send(outcome);
254        });
255        Self { rx }
256    }
257
258    /// Relay completion through a cleanup future that can inspect the outcome.
259    ///
260    /// The cleanup future receives a runtime-minted cleanup observation and
261    /// cannot replace the completion result.
262    pub fn with_outcome_cleanup<F, Fut>(self, cleanup: F) -> Self
263    where
264        F: FnOnce(CompletionCleanupObservation) -> Fut + Send + 'static,
265        Fut: Future<Output = ()> + Send + 'static,
266    {
267        let (tx, rx) = oneshot::channel();
268        crate::tokio::spawn(async move {
269            let outcome = match self.try_wait_delivery().await {
270                Ok(delivery) => {
271                    cleanup(delivery.cleanup_observation.clone()).await;
272                    Ok(delivery)
273                }
274                Err(error) => Err(error),
275            };
276            let _ = tx.send(outcome);
277        });
278        Self { rx }
279    }
280
281    /// Relay completion through cleanup that can observe either generated
282    /// completion-cleanup evidence or a typed waiter failure.
283    pub fn with_completion_cleanup<F, Fut>(self, cleanup: F) -> Self
284    where
285        F: FnOnce(Result<CompletionCleanupObservation, CompletionWaitError>) -> Fut
286            + Send
287            + 'static,
288        Fut: Future<Output = ()> + Send + 'static,
289    {
290        let (tx, rx) = oneshot::channel();
291        crate::tokio::spawn(async move {
292            let outcome = self.try_wait_delivery().await;
293            match &outcome {
294                Ok(delivery) => cleanup(Ok(delivery.cleanup_observation.clone())).await,
295                Err(error) => cleanup(Err(error.clone())).await,
296            }
297            let _ = tx.send(outcome);
298        });
299        Self { rx }
300    }
301
302    /// Relay completion through required, fallible cleanup before publishing
303    /// the outcome. A cleanup failure withholds a successful runtime result;
304    /// when waiter delivery and cleanup both fail, both causes are preserved.
305    pub fn with_resultful_completion_cleanup<F, Fut>(self, cleanup: F) -> Self
306    where
307        F: FnOnce(Result<CompletionCleanupObservation, CompletionWaitError>) -> Fut
308            + Send
309            + 'static,
310        Fut: Future<Output = Result<(), CompletionWaitError>> + Send + 'static,
311    {
312        let (tx, rx) = oneshot::channel();
313        crate::tokio::spawn(async move {
314            let outcome = self.try_wait_delivery().await;
315            let cleanup_input = match &outcome {
316                Ok(delivery) => Ok(delivery.cleanup_observation.clone()),
317                Err(error) => Err(error.clone()),
318            };
319            let cleanup_result = cleanup(cleanup_input).await;
320            let gated = match (outcome, cleanup_result) {
321                (Ok(delivery), Ok(())) => Ok(delivery),
322                (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
323                (Err(primary_error), Ok(())) => Err(primary_error),
324                (Err(primary_error), Err(cleanup_error)) => {
325                    Err(CompletionWaitError::AuthorityUnavailable(format!(
326                        "{primary_error}; additionally failed required completion cleanup: {cleanup_error}"
327                    )))
328                }
329            };
330            let _ = tx.send(gated);
331        });
332        Self { rx }
333    }
334
335    #[cfg(test)]
336    fn already_resolved_internal(
337        outcome: CompletionOutcome,
338        realized: RuntimeCompletionResultRealized,
339    ) -> Self {
340        let (tx, rx) = oneshot::channel();
341        let _ = tx.send(Ok(CompletionDelivery {
342            outcome,
343            cleanup_observation: CompletionCleanupObservation::from_realized_result(realized),
344        }));
345        Self { rx }
346    }
347
348    #[cfg(test)]
349    pub(crate) fn already_resolved_with_generated_class(
350        outcome: CompletionOutcome,
351        expected_class: crate::meerkat_machine::dsl::RuntimeCompletionResultClass,
352        terminal: crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation,
353        finalization: crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation,
354    ) -> Result<Self, crate::RuntimeDriverError> {
355        let run_id = if terminal
356            == crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::RuntimeTerminated
357        {
358            None
359        } else {
360            Some(RunId::new())
361        };
362        let authority =
363            crate::meerkat_machine::driver::machine_resolve_pre_resolved_runtime_completion_result(
364                run_id.as_ref(),
365                terminal,
366                finalization,
367            )?;
368        let attempt = authority.begin_surface_resolution();
369        if !attempt.allows(expected_class) {
370            let generated_class = attempt.class();
371            attempt.fail();
372            return Err(crate::RuntimeDriverError::Internal(format!(
373                "generated runtime completion authority returned {generated_class:?}, expected {expected_class:?}",
374            )));
375        }
376        Ok(Self::already_resolved_internal(outcome, attempt.realize()))
377    }
378
379    #[cfg(test)]
380    pub(crate) fn already_completed_without_result() -> Result<Self, crate::RuntimeDriverError> {
381        Self::already_resolved_with_generated_class(
382            CompletionOutcome::CompletedWithoutResult,
383            crate::meerkat_machine::dsl::RuntimeCompletionResultClass::CompletedWithoutResult,
384            crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::NoResult,
385            crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
386        )
387    }
388
389    #[cfg(test)]
390    pub(crate) fn already_runtime_apply_failed(
391        reason: String,
392        error: TurnErrorMetadata,
393    ) -> Result<Self, crate::RuntimeDriverError> {
394        Self::already_resolved_with_generated_class(
395            CompletionOutcome::AbandonedWithError { reason, error },
396            crate::meerkat_machine::dsl::RuntimeCompletionResultClass::AbandonedWithError,
397            crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::NoResult,
398            crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Failed,
399        )
400    }
401
402    #[cfg(test)]
403    pub(crate) fn already_runtime_terminated(
404        reason: String,
405    ) -> Result<Self, crate::RuntimeDriverError> {
406        Self::already_resolved_with_generated_class(
407            CompletionOutcome::runtime_terminated(&reason),
408            crate::meerkat_machine::dsl::RuntimeCompletionResultClass::RuntimeTerminated,
409            crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::RuntimeTerminated,
410            crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
411        )
412    }
413
414    #[cfg(test)]
415    pub(crate) fn already_callback_pending(
416        tool_name: String,
417        args: Value,
418    ) -> Result<Self, crate::RuntimeDriverError> {
419        Self::already_resolved_with_generated_class(
420            CompletionOutcome::CallbackPending { tool_name, args },
421            crate::meerkat_machine::dsl::RuntimeCompletionResultClass::CallbackPending,
422            crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::CallbackPending,
423            crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
424        )
425    }
426}
427
428impl CompletionOutcome {
429    /// Mint a [`RuntimeTerminated`](Self::RuntimeTerminated) outcome from a
430    /// termination reason, attaching the typed terminal failure metadata every
431    /// surface keys off (runtime stop/destroy is a fatal terminal boundary).
432    fn runtime_terminated(reason: &str) -> Self {
433        Self::RuntimeTerminated {
434            reason: reason.to_string(),
435            error: TurnErrorMetadata::terminal(
436                TurnTerminalCauseKind::FatalFailure,
437                TurnTerminalOutcome::Failed,
438                reason,
439            ),
440        }
441    }
442
443    pub fn abandoned_reason(&self) -> Option<&str> {
444        match self {
445            Self::Abandoned { reason, .. } | Self::AbandonedWithError { reason, .. } => {
446                Some(reason)
447            }
448            _ => None,
449        }
450    }
451
452    pub fn error_metadata(&self) -> Option<&TurnErrorMetadata> {
453        match self {
454            Self::Abandoned { error, .. }
455            | Self::AbandonedWithError { error, .. }
456            | Self::CompletedWithFinalizationFailure { error, .. }
457            | Self::RuntimeTerminated { error, .. } => Some(error),
458            _ => None,
459        }
460    }
461}
462
463/// Registry of pending completion waiters, keyed by InputId.
464///
465/// Uses `Vec<Sender>` per InputId to support multiple waiters for the same input
466/// (e.g. dedup of in-flight input registers a second waiter for the same InputId).
467#[derive(Default)]
468pub(crate) struct CompletionRegistry {
469    waiters:
470        HashMap<InputId, Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>>,
471}
472
473impl CompletionRegistry {
474    pub(crate) fn new() -> Self {
475        Self::default()
476    }
477
478    fn take_waiters(
479        &mut self,
480        input_id: &InputId,
481    ) -> Option<Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>> {
482        self.waiters.remove(input_id)
483    }
484
485    fn send_outcome(
486        senders: Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>,
487        outcome: CompletionOutcome,
488        cleanup_observation: CompletionCleanupObservation,
489    ) {
490        for tx in senders {
491            let outcome = match &outcome {
492                CompletionOutcome::Completed(result) => {
493                    CompletionOutcome::Completed(Box::new(result.as_ref().clone()))
494                }
495                CompletionOutcome::CompletedWithoutResult => {
496                    CompletionOutcome::CompletedWithoutResult
497                }
498                CompletionOutcome::CallbackPending { tool_name, args } => {
499                    CompletionOutcome::CallbackPending {
500                        tool_name: tool_name.clone(),
501                        args: args.clone(),
502                    }
503                }
504                CompletionOutcome::Cancelled => CompletionOutcome::Cancelled,
505                CompletionOutcome::Abandoned { reason, error } => CompletionOutcome::Abandoned {
506                    reason: reason.clone(),
507                    error: error.clone(),
508                },
509                CompletionOutcome::AbandonedWithError { reason, error } => {
510                    CompletionOutcome::AbandonedWithError {
511                        reason: reason.clone(),
512                        error: error.clone(),
513                    }
514                }
515                CompletionOutcome::CompletedWithFinalizationFailure { error } => {
516                    CompletionOutcome::CompletedWithFinalizationFailure {
517                        error: error.clone(),
518                    }
519                }
520                CompletionOutcome::RuntimeTerminated { reason, error } => {
521                    CompletionOutcome::RuntimeTerminated {
522                        reason: reason.clone(),
523                        error: error.clone(),
524                    }
525                }
526            };
527            let _ = tx.send(Ok(CompletionDelivery {
528                outcome,
529                cleanup_observation: cleanup_observation.clone(),
530            }));
531        }
532    }
533
534    fn send_error(
535        senders: Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>,
536        error: CompletionWaitError,
537    ) {
538        for tx in senders {
539            let _ = tx.send(Err(error.clone()));
540        }
541    }
542
543    fn authority_mismatch_error(
544        authority: &RuntimeCompletionResultAttempt,
545        expected: RuntimeCompletionResultClass,
546    ) -> CompletionWaitError {
547        CompletionWaitError::AuthorityUnavailable(format!(
548            "generated runtime completion authority returned {:?}, expected {expected:?}",
549            authority.class()
550        ))
551    }
552
553    fn fail_input_authority_mismatch(
554        &mut self,
555        input_id: &InputId,
556        authority: RuntimeCompletionResultAttempt,
557        expected: RuntimeCompletionResultClass,
558    ) {
559        let error = Self::authority_mismatch_error(&authority, expected);
560        authority.fail();
561        if let Some(senders) = self.take_waiters(input_id) {
562            Self::send_error(senders, error);
563        }
564    }
565
566    #[cfg(test)]
567    fn fail_inputs_authority_mismatch<I>(
568        &mut self,
569        input_ids: I,
570        authority: RuntimeCompletionResultAttempt,
571        expected: RuntimeCompletionResultClass,
572    ) where
573        I: IntoIterator<Item = InputId>,
574    {
575        let error = Self::authority_mismatch_error(&authority, expected);
576        authority.fail();
577        self.fail_inputs(input_ids, error);
578    }
579
580    fn fail_inputs_authority_unavailable<I>(
581        &mut self,
582        input_ids: I,
583        authority: RuntimeCompletionResultAttempt,
584        reason: impl Into<String>,
585    ) where
586        I: IntoIterator<Item = InputId>,
587    {
588        authority.fail();
589        self.fail_inputs(
590            input_ids,
591            CompletionWaitError::AuthorityUnavailable(reason.into()),
592        );
593    }
594
595    fn cleanup_from_realized_attempt(
596        authority: RuntimeCompletionResultAttempt,
597    ) -> CompletionCleanupObservation {
598        CompletionCleanupObservation::from_realized_result(authority.realize())
599    }
600
601    /// Register a waiter for an input. Returns the handle the caller will await.
602    ///
603    /// Multiple waiters can be registered for the same InputId — all will be
604    /// resolved when the input reaches a terminal state.
605    pub(crate) fn register(&mut self, input_id: InputId) -> CompletionHandle {
606        let (tx, rx) = oneshot::channel();
607        self.waiters.entry(input_id).or_default().push(tx);
608        CompletionHandle { rx }
609    }
610
611    /// Resolve all waiters for a completed input.
612    fn resolve_completed(
613        &mut self,
614        input_id: &InputId,
615        result: RunResult,
616        cleanup_observation: CompletionCleanupObservation,
617    ) {
618        if let Some(senders) = self.take_waiters(input_id) {
619            Self::send_outcome(
620                senders,
621                CompletionOutcome::Completed(Box::new(result)),
622                cleanup_observation,
623            );
624        }
625    }
626
627    #[cfg(test)]
628    pub(crate) fn resolve_completed_authorized(
629        &mut self,
630        input_id: &InputId,
631        result: RunResult,
632        authority: RuntimeCompletionResultAuthority,
633    ) {
634        let expected = RuntimeCompletionResultClass::Completed;
635        let attempt = authority.begin_surface_resolution();
636        if !attempt.allows(expected) {
637            self.fail_input_authority_mismatch(input_id, attempt, expected);
638            return;
639        }
640        self.resolve_completed(
641            input_id,
642            result,
643            Self::cleanup_from_realized_attempt(attempt),
644        );
645    }
646
647    pub(crate) fn resolve_runtime_completion_authorized<I>(
648        &mut self,
649        input_ids: I,
650        terminal: Option<&CoreApplyTerminal>,
651        authority: RuntimeCompletionResultAuthority,
652        finalization_error: Option<TurnErrorMetadata>,
653    ) where
654        I: IntoIterator<Item = InputId>,
655    {
656        let input_ids: Vec<InputId> = input_ids.into_iter().collect();
657        let attempt = authority.begin_surface_resolution();
658        match attempt.class() {
659            RuntimeCompletionResultClass::Completed => {
660                let Some(CoreApplyTerminal::RunResult(result)) = terminal else {
661                    self.fail_inputs_authority_unavailable(
662                        input_ids,
663                        attempt,
664                        "runtime completion authority resolved Completed without result payload",
665                    );
666                    return;
667                };
668                let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
669                for input_id in input_ids {
670                    self.resolve_completed(
671                        &input_id,
672                        result.as_ref().clone(),
673                        cleanup_observation.clone(),
674                    );
675                }
676            }
677            RuntimeCompletionResultClass::CompletedWithoutResult => {
678                if !matches!(terminal, Some(CoreApplyTerminal::NoPendingBoundary) | None) {
679                    self.fail_inputs_authority_unavailable(
680                        input_ids,
681                        attempt,
682                        "runtime completion authority resolved CompletedWithoutResult with terminal payload",
683                    );
684                    return;
685                }
686                let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
687                for input_id in input_ids {
688                    self.resolve_without_result(&input_id, cleanup_observation.clone());
689                }
690            }
691            RuntimeCompletionResultClass::CallbackPending => {
692                let Some(CoreApplyTerminal::CallbackPending { tool_name, args }) = terminal else {
693                    self.fail_inputs_authority_unavailable(
694                        input_ids,
695                        attempt,
696                        "runtime completion authority resolved CallbackPending without callback payload",
697                    );
698                    return;
699                };
700                let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
701                for input_id in input_ids {
702                    self.resolve_callback_pending(
703                        &input_id,
704                        tool_name.clone(),
705                        args.clone(),
706                        cleanup_observation.clone(),
707                    );
708                }
709            }
710            RuntimeCompletionResultClass::Cancelled => {
711                if terminal.is_some() || finalization_error.is_some() {
712                    self.fail_inputs_authority_unavailable(
713                        input_ids,
714                        attempt,
715                        "runtime completion authority resolved Cancelled with payload",
716                    );
717                    return;
718                }
719                let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
720                for input_id in input_ids {
721                    self.resolve_cancelled(&input_id, cleanup_observation.clone());
722                }
723            }
724            RuntimeCompletionResultClass::AbandonedWithError => {
725                if matches!(terminal, Some(CoreApplyTerminal::RunResult(_))) {
726                    self.fail_inputs_authority_unavailable(
727                        input_ids,
728                        attempt,
729                        "runtime completion authority resolved AbandonedWithError with result payload",
730                    );
731                    return;
732                }
733                let Some(error) = finalization_error else {
734                    self.fail_inputs_authority_unavailable(
735                        input_ids,
736                        attempt,
737                        "runtime completion authority resolved AbandonedWithError without typed error",
738                    );
739                    return;
740                };
741                let reason = error
742                    .detail
743                    .clone()
744                    .unwrap_or_else(|| "runtime finalization failed".to_string());
745                let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
746                for input_id in input_ids {
747                    self.resolve_abandoned_with_error(
748                        &input_id,
749                        reason.clone(),
750                        error.clone(),
751                        cleanup_observation.clone(),
752                    );
753                }
754            }
755            RuntimeCompletionResultClass::CompletedWithFinalizationFailure => {
756                let Some(CoreApplyTerminal::RunResult(_result)) = terminal else {
757                    self.fail_inputs_authority_unavailable(
758                        input_ids,
759                        attempt,
760                        "runtime completion authority resolved CompletedWithFinalizationFailure without result payload",
761                    );
762                    return;
763                };
764                let Some(error) = finalization_error else {
765                    self.fail_inputs_authority_unavailable(
766                        input_ids,
767                        attempt,
768                        "runtime completion authority resolved finalization failure without typed error",
769                    );
770                    return;
771                };
772                let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
773                for input_id in input_ids {
774                    self.resolve_completed_with_finalization_failure(
775                        &input_id,
776                        error.clone(),
777                        cleanup_observation.clone(),
778                    );
779                }
780            }
781            RuntimeCompletionResultClass::RuntimeTerminated => {
782                if terminal.is_some() || finalization_error.is_some() {
783                    self.fail_inputs_authority_unavailable(
784                        input_ids,
785                        attempt,
786                        "runtime completion authority resolved RuntimeTerminated with payload",
787                    );
788                    return;
789                }
790                let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
791                for input_id in input_ids {
792                    if let Some(senders) = self.take_waiters(&input_id) {
793                        Self::send_outcome(
794                            senders,
795                            CompletionOutcome::runtime_terminated("runtime terminated"),
796                            cleanup_observation.clone(),
797                        );
798                    }
799                }
800            }
801        }
802    }
803
804    /// Resolve all waiters for an input that completed without producing a RunResult.
805    fn resolve_without_result(
806        &mut self,
807        input_id: &InputId,
808        cleanup_observation: CompletionCleanupObservation,
809    ) {
810        if let Some(senders) = self.take_waiters(input_id) {
811            Self::send_outcome(
812                senders,
813                CompletionOutcome::CompletedWithoutResult,
814                cleanup_observation,
815            );
816        }
817    }
818
819    pub(crate) fn resolve_without_result_authorized(
820        &mut self,
821        input_id: &InputId,
822        authority: RuntimeCompletionResultAuthority,
823    ) {
824        let expected = RuntimeCompletionResultClass::CompletedWithoutResult;
825        let attempt = authority.begin_surface_resolution();
826        if !attempt.allows(expected) {
827            self.fail_input_authority_mismatch(input_id, attempt, expected);
828            return;
829        }
830        self.resolve_without_result(input_id, Self::cleanup_from_realized_attempt(attempt));
831    }
832
833    /// Resolve all waiters for an input that reached a callback boundary.
834    fn resolve_callback_pending(
835        &mut self,
836        input_id: &InputId,
837        tool_name: String,
838        args: Value,
839        cleanup_observation: CompletionCleanupObservation,
840    ) {
841        if let Some(senders) = self.take_waiters(input_id) {
842            Self::send_outcome(
843                senders,
844                CompletionOutcome::CallbackPending { tool_name, args },
845                cleanup_observation,
846            );
847        }
848    }
849
850    #[cfg(test)]
851    pub(crate) fn resolve_callback_pending_authorized(
852        &mut self,
853        input_id: &InputId,
854        tool_name: String,
855        args: Value,
856        authority: RuntimeCompletionResultAuthority,
857    ) {
858        let expected = RuntimeCompletionResultClass::CallbackPending;
859        let attempt = authority.begin_surface_resolution();
860        if !attempt.allows(expected) {
861            self.fail_input_authority_mismatch(input_id, attempt, expected);
862            return;
863        }
864        self.resolve_callback_pending(
865            input_id,
866            tool_name,
867            args,
868            Self::cleanup_from_realized_attempt(attempt),
869        );
870    }
871
872    /// Resolve all waiters for an input that reached the cancellation terminal.
873    fn resolve_cancelled(
874        &mut self,
875        input_id: &InputId,
876        cleanup_observation: CompletionCleanupObservation,
877    ) {
878        if let Some(senders) = self.take_waiters(input_id) {
879            Self::send_outcome(senders, CompletionOutcome::Cancelled, cleanup_observation);
880        }
881    }
882
883    #[cfg(test)]
884    pub(crate) fn resolve_cancelled_authorized(
885        &mut self,
886        input_id: &InputId,
887        authority: RuntimeCompletionResultAuthority,
888    ) {
889        let expected = RuntimeCompletionResultClass::Cancelled;
890        let attempt = authority.begin_surface_resolution();
891        if !attempt.allows(expected) {
892            self.fail_input_authority_mismatch(input_id, attempt, expected);
893            return;
894        }
895        self.resolve_cancelled(input_id, Self::cleanup_from_realized_attempt(attempt));
896    }
897
898    /// Resolve all waiters for an abandoned input with typed failure metadata.
899    fn resolve_abandoned_with_error(
900        &mut self,
901        input_id: &InputId,
902        reason: String,
903        error: TurnErrorMetadata,
904        cleanup_observation: CompletionCleanupObservation,
905    ) {
906        if let Some(senders) = self.take_waiters(input_id) {
907            Self::send_outcome(
908                senders,
909                CompletionOutcome::AbandonedWithError { reason, error },
910                cleanup_observation,
911            );
912        }
913    }
914
915    #[cfg(test)]
916    pub(crate) fn resolve_abandoned_with_error_authorized(
917        &mut self,
918        input_id: &InputId,
919        reason: String,
920        error: TurnErrorMetadata,
921        authority: RuntimeCompletionResultAuthority,
922    ) {
923        let expected = RuntimeCompletionResultClass::AbandonedWithError;
924        let attempt = authority.begin_surface_resolution();
925        if !attempt.allows(expected) {
926            self.fail_input_authority_mismatch(input_id, attempt, expected);
927            return;
928        }
929        self.resolve_abandoned_with_error(
930            input_id,
931            reason,
932            error,
933            Self::cleanup_from_realized_attempt(attempt),
934        );
935    }
936
937    /// Resolve all waiters for a turn whose output exists but finalization
938    /// failed after output production.
939    fn resolve_completed_with_finalization_failure(
940        &mut self,
941        input_id: &InputId,
942        error: TurnErrorMetadata,
943        cleanup_observation: CompletionCleanupObservation,
944    ) {
945        if let Some(senders) = self.take_waiters(input_id) {
946            Self::send_outcome(
947                senders,
948                CompletionOutcome::CompletedWithFinalizationFailure { error },
949                cleanup_observation,
950            );
951        }
952    }
953
954    #[cfg(test)]
955    pub(crate) fn resolve_completed_with_finalization_failure_authorized(
956        &mut self,
957        input_id: &InputId,
958        error: TurnErrorMetadata,
959        authority: RuntimeCompletionResultAuthority,
960    ) {
961        let expected = RuntimeCompletionResultClass::CompletedWithFinalizationFailure;
962        let attempt = authority.begin_surface_resolution();
963        if !attempt.allows(expected) {
964            self.fail_input_authority_mismatch(input_id, attempt, expected);
965            return;
966        }
967        self.resolve_completed_with_finalization_failure(
968            input_id,
969            error,
970            Self::cleanup_from_realized_attempt(attempt),
971        );
972    }
973
974    /// Resolve all pending waiters with a termination error.
975    ///
976    /// The public termination result class is supplied by generated
977    /// MeerkatMachine authority; this registry method only fans the authorized
978    /// class out to waiter channels.
979    pub(crate) fn resolve_all_runtime_terminated(
980        &mut self,
981        reason: &str,
982        authority: RuntimeCompletionResultAuthority,
983    ) {
984        let expected = RuntimeCompletionResultClass::RuntimeTerminated;
985        let attempt = authority.begin_surface_resolution();
986        if !attempt.allows(expected) {
987            let error = Self::authority_mismatch_error(&attempt, expected);
988            attempt.fail();
989            self.fail_all_waiters(error);
990            return;
991        }
992        let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
993        for (_, senders) in self.waiters.drain() {
994            Self::send_outcome(
995                senders,
996                CompletionOutcome::runtime_terminated(reason),
997                cleanup_observation.clone(),
998            );
999        }
1000    }
1001
1002    #[cfg(test)]
1003    pub(crate) fn resolve_inputs_runtime_terminated<I>(
1004        &mut self,
1005        input_ids: I,
1006        reason: &str,
1007        authority: RuntimeCompletionResultAuthority,
1008    ) where
1009        I: IntoIterator<Item = InputId>,
1010    {
1011        let input_ids: Vec<InputId> = input_ids.into_iter().collect();
1012        let expected = RuntimeCompletionResultClass::RuntimeTerminated;
1013        let attempt = authority.begin_surface_resolution();
1014        if !attempt.allows(expected) {
1015            self.fail_inputs_authority_mismatch(input_ids, attempt, expected);
1016            return;
1017        }
1018        let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
1019        for input_id in input_ids {
1020            if let Some(senders) = self.take_waiters(&input_id) {
1021                Self::send_outcome(
1022                    senders,
1023                    CompletionOutcome::runtime_terminated(reason),
1024                    cleanup_observation.clone(),
1025                );
1026            }
1027        }
1028    }
1029
1030    pub(crate) fn fail_all_waiters(&mut self, error: CompletionWaitError) {
1031        for (_, senders) in self.waiters.drain() {
1032            Self::send_error(senders, error.clone());
1033        }
1034    }
1035
1036    pub(crate) fn fail_inputs<I>(&mut self, input_ids: I, error: CompletionWaitError)
1037    where
1038        I: IntoIterator<Item = InputId>,
1039    {
1040        for input_id in input_ids {
1041            if let Some(senders) = self.take_waiters(&input_id) {
1042                Self::send_error(senders, error.clone());
1043            }
1044        }
1045    }
1046
1047    /// Resolve waiters whose input IDs are no longer pending after a
1048    /// lifecycle reconciliation (for example runtime recycle/recovery).
1049    pub(crate) fn resolve_not_pending_runtime_terminated<F>(
1050        &mut self,
1051        mut is_still_pending: F,
1052        reason: &str,
1053        authority: RuntimeCompletionResultAuthority,
1054    ) where
1055        F: FnMut(&InputId) -> bool,
1056    {
1057        let expected = RuntimeCompletionResultClass::RuntimeTerminated;
1058        let attempt = authority.begin_surface_resolution();
1059        if !attempt.allows(expected) {
1060            let error = Self::authority_mismatch_error(&attempt, expected);
1061            attempt.fail();
1062            self.waiters.retain(|input_id, senders| {
1063                if is_still_pending(input_id) {
1064                    return true;
1065                }
1066
1067                Self::send_error(std::mem::take(senders), error.clone());
1068                false
1069            });
1070            return;
1071        }
1072
1073        let mut ready = Vec::new();
1074        self.waiters.retain(|input_id, senders| {
1075            if is_still_pending(input_id) {
1076                return true;
1077            }
1078
1079            ready.push(std::mem::take(senders));
1080            false
1081        });
1082        if ready.is_empty() {
1083            attempt.abandon();
1084            return;
1085        }
1086        let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
1087        for senders in ready {
1088            Self::send_outcome(
1089                senders,
1090                CompletionOutcome::runtime_terminated(reason),
1091                cleanup_observation.clone(),
1092            );
1093        }
1094    }
1095
1096    /// Snapshot the current waiter carrier without mutating it.
1097    pub(crate) fn diagnostic_snapshot(&self) -> CompletionRegistrySnapshot {
1098        let mut waiting_inputs: Vec<_> = self
1099            .waiters
1100            .iter()
1101            .map(|(input_id, senders)| CompletionWaiterEntrySnapshot {
1102                input_id: input_id.clone(),
1103                waiter_count: senders.len(),
1104            })
1105            .collect();
1106        waiting_inputs
1107            .sort_by(|left, right| left.input_id.to_string().cmp(&right.input_id.to_string()));
1108
1109        CompletionRegistrySnapshot {
1110            input_count: waiting_inputs.len(),
1111            waiter_count: waiting_inputs.iter().map(|entry| entry.waiter_count).sum(),
1112            waiting_inputs,
1113        }
1114    }
1115
1116    /// Check if there are any pending waiters.
1117    ///
1118    /// Test-only introspection. Production code must treat the registry as
1119    /// waiter plumbing rather than semantic runtime truth.
1120    #[cfg(test)]
1121    pub fn debug_has_waiters(&self) -> bool {
1122        !self.waiters.is_empty()
1123    }
1124
1125    /// Number of pending waiters (total across all InputIds).
1126    ///
1127    /// Test-only introspection. Production code must treat the registry as
1128    /// waiter plumbing rather than semantic runtime truth.
1129    #[cfg(test)]
1130    pub fn debug_waiter_count(&self) -> usize {
1131        self.waiters.values().map(Vec::len).sum()
1132    }
1133}
1134
1135#[cfg(test)]
1136#[allow(clippy::unwrap_used, clippy::panic)]
1137mod tests {
1138    use super::*;
1139    use crate::meerkat_machine::dsl::{
1140        RuntimeCompletionObservedOutcome, RuntimeCompletionResultClass,
1141    };
1142    use meerkat_core::types::{SessionId, Usage};
1143
1144    fn make_run_result() -> RunResult {
1145        RunResult {
1146            text: "hello".into(),
1147            session_id: SessionId::new(),
1148            usage: Usage::default(),
1149            turns: 1,
1150            tool_calls: 0,
1151            terminal_cause_kind: None,
1152            structured_output: None,
1153            extraction_error: None,
1154            schema_warnings: None,
1155            skill_diagnostics: None,
1156        }
1157    }
1158
1159    fn authority(
1160        result_class: RuntimeCompletionResultClass,
1161        cleanup_observation: RuntimeCompletionObservedOutcome,
1162    ) -> RuntimeCompletionResultAuthority {
1163        crate::meerkat_machine::driver::test_runtime_completion_authority(
1164            result_class,
1165            cleanup_observation,
1166        )
1167    }
1168
1169    #[tokio::test]
1170    async fn register_and_complete() {
1171        let mut registry = CompletionRegistry::new();
1172        let input_id = InputId::new();
1173        let handle = registry.register(input_id.clone());
1174
1175        assert!(registry.debug_has_waiters());
1176        assert_eq!(registry.debug_waiter_count(), 1);
1177
1178        let result = make_run_result();
1179        registry.resolve_completed_authorized(
1180            &input_id,
1181            result,
1182            authority(
1183                RuntimeCompletionResultClass::Completed,
1184                RuntimeCompletionObservedOutcome::Completed,
1185            ),
1186        );
1187
1188        match handle.wait_authorized().await {
1189            CompletionOutcome::Completed(r) => assert_eq!(r.text, "hello"),
1190            other => panic!("Expected Completed, got {other:?}"),
1191        }
1192    }
1193
1194    #[tokio::test]
1195    async fn register_and_fail_waiter() {
1196        let mut registry = CompletionRegistry::new();
1197        let input_id = InputId::new();
1198        let handle = registry.register(input_id.clone());
1199
1200        registry.fail_inputs(
1201            [input_id],
1202            CompletionWaitError::AuthorityUnavailable("retired".into()),
1203        );
1204
1205        match handle.try_wait().await {
1206            Err(CompletionWaitError::AuthorityUnavailable(reason)) => assert_eq!(reason, "retired"),
1207            other => panic!("Expected wait error, got {other:?}"),
1208        }
1209    }
1210
1211    #[tokio::test]
1212    async fn mismatched_result_authority_fails_waiter_closed() {
1213        let mut registry = CompletionRegistry::new();
1214        let input_id = InputId::new();
1215        let handle = registry.register(input_id.clone());
1216
1217        registry.resolve_completed_authorized(
1218            &input_id,
1219            make_run_result(),
1220            authority(
1221                RuntimeCompletionResultClass::Cancelled,
1222                RuntimeCompletionObservedOutcome::Cancelled,
1223            ),
1224        );
1225
1226        assert!(!registry.debug_has_waiters());
1227        match handle.try_wait().await {
1228            Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1229                assert!(reason.contains("Cancelled"));
1230                assert!(reason.contains("Completed"));
1231            }
1232            other => panic!("Expected authority mismatch wait error, got {other:?}"),
1233        }
1234    }
1235
1236    #[tokio::test]
1237    async fn resolve_all_runtime_terminated() {
1238        let mut registry = CompletionRegistry::new();
1239        let h1 = registry.register(InputId::new());
1240        let h2 = registry.register(InputId::new());
1241
1242        registry.resolve_all_runtime_terminated(
1243            "runtime stopped",
1244            authority(
1245                RuntimeCompletionResultClass::RuntimeTerminated,
1246                RuntimeCompletionObservedOutcome::RuntimeTerminated,
1247            ),
1248        );
1249
1250        assert!(!registry.debug_has_waiters());
1251
1252        match h1.wait_authorized().await {
1253            CompletionOutcome::RuntimeTerminated { reason, .. } => {
1254                assert_eq!(reason, "runtime stopped");
1255            }
1256            other => panic!("Expected RuntimeTerminated, got {other:?}"),
1257        }
1258        match h2.wait_authorized().await {
1259            CompletionOutcome::RuntimeTerminated { reason, .. } => {
1260                assert_eq!(reason, "runtime stopped");
1261            }
1262            other => panic!("Expected RuntimeTerminated, got {other:?}"),
1263        }
1264    }
1265
1266    #[tokio::test]
1267    async fn mismatched_runtime_terminated_authority_fails_all_waiters_closed() {
1268        let mut registry = CompletionRegistry::new();
1269        let h1 = registry.register(InputId::new());
1270        let h2 = registry.register(InputId::new());
1271
1272        registry.resolve_all_runtime_terminated(
1273            "runtime stopped",
1274            authority(
1275                RuntimeCompletionResultClass::CompletedWithoutResult,
1276                RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1277            ),
1278        );
1279
1280        assert!(!registry.debug_has_waiters());
1281        for handle in [h1, h2] {
1282            match handle.try_wait().await {
1283                Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1284                    assert!(reason.contains("CompletedWithoutResult"));
1285                    assert!(reason.contains("RuntimeTerminated"));
1286                }
1287                other => panic!("Expected authority mismatch wait error, got {other:?}"),
1288            }
1289        }
1290    }
1291
1292    #[tokio::test]
1293    async fn cleanup_rejects_observation_from_another_session() {
1294        let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1295        let source_session_id = SessionId::new();
1296        let target_session_id = SessionId::new();
1297        adapter
1298            .prepare_bindings(source_session_id.clone())
1299            .await
1300            .expect("source session should prepare runtime bindings");
1301        adapter
1302            .prepare_bindings(target_session_id.clone())
1303            .await
1304            .expect("target session should prepare runtime bindings");
1305
1306        let input = crate::Input::Prompt(crate::PromptInput::new(
1307            "source session pending completion",
1308            None,
1309        ));
1310        let (_outcome, handle) = adapter
1311            .accept_input_with_completion(&source_session_id, input)
1312            .await
1313            .expect("source input should be accepted");
1314        let handle = handle.expect("source input should have a completion waiter");
1315        adapter
1316            .stop_runtime_executor(&source_session_id, "source stopped")
1317            .await
1318            .expect("source stop should resolve waiter");
1319        let (_outcome, observation) = handle
1320            .try_wait_with_cleanup_observation()
1321            .await
1322            .expect("waiter should resolve with generated cleanup observation");
1323
1324        assert_eq!(observation.owner_session_id(), &source_session_id);
1325        let err = adapter
1326            .resolve_runtime_completion_cleanup(
1327                &target_session_id,
1328                observation,
1329                false,
1330                crate::meerkat_machine::dsl::RuntimeCompletionLiveSessionObservation::Absent,
1331            )
1332            .await
1333            .expect_err("cleanup must reject an observation minted for another session");
1334        assert!(
1335            matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1336            "expected generated cleanup validation failure, got {err:?}"
1337        );
1338    }
1339
1340    #[tokio::test]
1341    async fn cleanup_rejects_stale_same_session_observation_after_rebinding() {
1342        let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1343        let session_id = SessionId::new();
1344        adapter
1345            .prepare_bindings(session_id.clone())
1346            .await
1347            .expect("session should prepare initial runtime bindings");
1348
1349        let input = crate::Input::Prompt(crate::PromptInput::new(
1350            "same session pending completion",
1351            None,
1352        ));
1353        let (_outcome, handle) = adapter
1354            .accept_input_with_completion(&session_id, input)
1355            .await
1356            .expect("input should be accepted");
1357        let handle = handle.expect("input should have a completion waiter");
1358        adapter
1359            .stop_runtime_executor(&session_id, "first runtime stopped")
1360            .await
1361            .expect("stop should resolve waiter");
1362        let (_outcome, stale_observation) = handle
1363            .try_wait_with_cleanup_observation()
1364            .await
1365            .expect("waiter should resolve with generated cleanup observation");
1366
1367        adapter
1368            .unregister_session(&session_id)
1369            .await
1370            .expect("initial runtime binding should unregister cleanly");
1371        adapter
1372            .prepare_bindings(session_id.clone())
1373            .await
1374            .expect("session should prepare replacement runtime bindings");
1375
1376        let err = adapter
1377            .resolve_runtime_completion_cleanup(
1378                &session_id,
1379                stale_observation,
1380                false,
1381                crate::meerkat_machine::dsl::RuntimeCompletionLiveSessionObservation::Absent,
1382            )
1383            .await
1384            .expect_err("cleanup must reject an observation minted for a prior runtime binding");
1385        assert!(
1386            matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1387            "expected generated cleanup validation failure, got {err:?}"
1388        );
1389    }
1390
1391    #[tokio::test]
1392    async fn wait_failure_authority_releases_pre_admission_and_classifies_public_reason() {
1393        let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1394        let session_id = SessionId::new();
1395        adapter
1396            .prepare_bindings(session_id.clone())
1397            .await
1398            .expect("session should prepare runtime bindings");
1399
1400        let authority = adapter
1401            .resolve_runtime_completion_wait_failure(
1402                &session_id,
1403                &CompletionWaitError::AuthorityUnavailable("missing generated result".into()),
1404            )
1405            .await
1406            .expect("wait-failure authority should resolve");
1407
1408        assert!(authority.releases_pre_admission());
1409        assert_eq!(
1410            authority.public_error_class,
1411            crate::meerkat_machine::dsl::RuntimeCompletionWaitFailurePublicErrorClass::InternalError
1412        );
1413        assert_eq!(
1414            authority.public_reason,
1415            crate::meerkat_machine::dsl::RuntimeCompletionWaitFailurePublicReason::CompletionAuthorityUnavailable
1416        );
1417        assert!(!authority.resumable);
1418    }
1419
1420    #[tokio::test]
1421    async fn wait_failure_authority_rejects_missing_session_authority() {
1422        let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1423        let session_id = SessionId::new();
1424
1425        let err = adapter
1426            .resolve_runtime_completion_wait_failure(
1427                &session_id,
1428                &CompletionWaitError::ChannelClosed,
1429            )
1430            .await
1431            .expect_err("wait-failure authority must fail closed without a session authority");
1432
1433        assert!(
1434            matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1435            "expected generated wait-failure validation failure, got {err:?}"
1436        );
1437    }
1438
1439    #[tokio::test]
1440    async fn resolve_nonexistent_is_a_noop() {
1441        let mut registry = CompletionRegistry::new();
1442        registry.resolve_completed_authorized(
1443            &InputId::new(),
1444            make_run_result(),
1445            authority(
1446                RuntimeCompletionResultClass::Completed,
1447                RuntimeCompletionObservedOutcome::Completed,
1448            ),
1449        );
1450        registry.fail_inputs(
1451            [InputId::new()],
1452            CompletionWaitError::AuthorityUnavailable("gone".into()),
1453        );
1454        assert!(!registry.debug_has_waiters());
1455    }
1456
1457    #[tokio::test]
1458    async fn dropped_sender_gives_wait_error() {
1459        let mut registry = CompletionRegistry::new();
1460        let input_id = InputId::new();
1461        let handle = registry.register(input_id);
1462
1463        // Drop the registry (and thus the sender)
1464        drop(registry);
1465
1466        assert!(matches!(
1467            handle.try_wait().await,
1468            Err(CompletionWaitError::ChannelClosed)
1469        ));
1470    }
1471
1472    #[tokio::test]
1473    async fn multi_waiter_all_receive_result() {
1474        let mut registry = CompletionRegistry::new();
1475        let input_id = InputId::new();
1476
1477        let h1 = registry.register(input_id.clone());
1478        let h2 = registry.register(input_id.clone());
1479        let h3 = registry.register(input_id.clone());
1480
1481        assert_eq!(registry.debug_waiter_count(), 3);
1482
1483        let result = make_run_result();
1484        registry.resolve_completed_authorized(
1485            &input_id,
1486            result,
1487            authority(
1488                RuntimeCompletionResultClass::Completed,
1489                RuntimeCompletionObservedOutcome::Completed,
1490            ),
1491        );
1492
1493        assert!(!registry.debug_has_waiters());
1494
1495        for handle in [h1, h2, h3] {
1496            match handle.wait_authorized().await {
1497                CompletionOutcome::Completed(r) => assert_eq!(r.text, "hello"),
1498                other => panic!("Expected Completed, got {other:?}"),
1499            }
1500        }
1501    }
1502
1503    #[tokio::test]
1504    async fn resolve_without_result_sends_variant() {
1505        let mut registry = CompletionRegistry::new();
1506        let input_id = InputId::new();
1507        let handle = registry.register(input_id.clone());
1508
1509        registry.resolve_without_result_authorized(
1510            &input_id,
1511            authority(
1512                RuntimeCompletionResultClass::CompletedWithoutResult,
1513                RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1514            ),
1515        );
1516
1517        match handle.wait_authorized().await {
1518            CompletionOutcome::CompletedWithoutResult => {}
1519            other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1520        }
1521    }
1522
1523    #[tokio::test]
1524    async fn resolve_without_result_multi_waiter() {
1525        let mut registry = CompletionRegistry::new();
1526        let input_id = InputId::new();
1527        let h1 = registry.register(input_id.clone());
1528        let h2 = registry.register(input_id.clone());
1529
1530        registry.resolve_without_result_authorized(
1531            &input_id,
1532            authority(
1533                RuntimeCompletionResultClass::CompletedWithoutResult,
1534                RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1535            ),
1536        );
1537
1538        for handle in [h1, h2] {
1539            match handle.wait_authorized().await {
1540                CompletionOutcome::CompletedWithoutResult => {}
1541                other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1542            }
1543        }
1544    }
1545
1546    #[tokio::test]
1547    async fn resolve_callback_pending_sends_variant() {
1548        let mut registry = CompletionRegistry::new();
1549        let input_id = InputId::new();
1550        let handle = registry.register(input_id.clone());
1551
1552        registry.resolve_callback_pending_authorized(
1553            &input_id,
1554            "browser".to_string(),
1555            serde_json::json!({ "url": "https://example.com" }),
1556            authority(
1557                RuntimeCompletionResultClass::CallbackPending,
1558                RuntimeCompletionObservedOutcome::CallbackPending,
1559            ),
1560        );
1561
1562        match handle.wait_authorized().await {
1563            CompletionOutcome::CallbackPending { tool_name, args } => {
1564                assert_eq!(tool_name, "browser");
1565                assert_eq!(args, serde_json::json!({ "url": "https://example.com" }));
1566            }
1567            other => panic!("Expected CallbackPending, got {other:?}"),
1568        }
1569    }
1570
1571    #[tokio::test]
1572    async fn resolve_cancelled_sends_variant() {
1573        let mut registry = CompletionRegistry::new();
1574        let input_id = InputId::new();
1575        let handle = registry.register(input_id.clone());
1576
1577        registry.resolve_cancelled_authorized(
1578            &input_id,
1579            authority(
1580                RuntimeCompletionResultClass::Cancelled,
1581                RuntimeCompletionObservedOutcome::Cancelled,
1582            ),
1583        );
1584
1585        match handle.wait_authorized().await {
1586            CompletionOutcome::Cancelled => {}
1587            other => panic!("Expected Cancelled, got {other:?}"),
1588        }
1589    }
1590
1591    #[tokio::test]
1592    async fn already_resolved_handle() {
1593        let handle = CompletionHandle::already_completed_without_result()
1594            .expect("generated completion authority should classify no-result completion");
1595        match handle.wait_authorized().await {
1596            CompletionOutcome::CompletedWithoutResult => {}
1597            other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1598        }
1599    }
1600
1601    #[tokio::test]
1602    async fn outcome_cleanup_observes_and_relays_result() {
1603        use std::sync::Arc;
1604        use std::sync::atomic::{AtomicBool, Ordering};
1605
1606        let mut registry = CompletionRegistry::new();
1607        let input_id = InputId::new();
1608        let handle = registry.register(input_id.clone());
1609        let observed = Arc::new(AtomicBool::new(false));
1610        let cleanup_observed = Arc::clone(&observed);
1611        let handle = handle.with_outcome_cleanup(move |observation| async move {
1612            if observation.observed_outcome()
1613                == crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome::CompletedWithoutResult
1614            {
1615                cleanup_observed.store(true, Ordering::Release);
1616            }
1617        });
1618
1619        registry.resolve_without_result_authorized(
1620            &input_id,
1621            authority(
1622                RuntimeCompletionResultClass::CompletedWithoutResult,
1623                RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1624            ),
1625        );
1626        match handle.wait_authorized().await {
1627            CompletionOutcome::CompletedWithoutResult => {}
1628            other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1629        }
1630        assert!(observed.load(Ordering::Acquire));
1631    }
1632
1633    #[tokio::test]
1634    async fn resultful_completion_cleanup_withholds_success_and_combines_failures() {
1635        let handle = CompletionHandle::already_completed_without_result()
1636            .expect("generated completion authority should classify no-result completion")
1637            .with_resultful_completion_cleanup(|_| async {
1638                Err(CompletionWaitError::AuthorityUnavailable(
1639                    "required cleanup failed".to_string(),
1640                ))
1641            });
1642        let error = handle
1643            .wait()
1644            .await
1645            .expect_err("required cleanup failure must withhold success");
1646        assert!(error.to_string().contains("required cleanup failed"));
1647
1648        let (tx, rx) = oneshot::channel();
1649        drop(tx);
1650        let handle =
1651            CompletionHandle { rx }.with_resultful_completion_cleanup(|completion| async move {
1652                assert!(matches!(
1653                    completion,
1654                    Err(CompletionWaitError::ChannelClosed)
1655                ));
1656                Err(CompletionWaitError::AuthorityUnavailable(
1657                    "cleanup authority failed".to_string(),
1658                ))
1659            });
1660        let error = handle
1661            .wait()
1662            .await
1663            .expect_err("two failures must remain a failure");
1664        let rendered = error.to_string();
1665        assert!(rendered.contains("completion channel closed"));
1666        assert!(rendered.contains("cleanup authority failed"));
1667    }
1668
1669    #[tokio::test]
1670    async fn multi_waiter_terminated_on_reset() {
1671        let mut registry = CompletionRegistry::new();
1672        let input_id = InputId::new();
1673        let h1 = registry.register(input_id.clone());
1674        let h2 = registry.register(input_id);
1675
1676        registry.resolve_all_runtime_terminated(
1677            "runtime reset",
1678            authority(
1679                RuntimeCompletionResultClass::RuntimeTerminated,
1680                RuntimeCompletionObservedOutcome::RuntimeTerminated,
1681            ),
1682        );
1683
1684        for handle in [h1, h2] {
1685            match handle.wait_authorized().await {
1686                CompletionOutcome::RuntimeTerminated { reason, .. } => {
1687                    assert_eq!(reason, "runtime reset");
1688                }
1689                other => panic!("Expected RuntimeTerminated, got {other:?}"),
1690            }
1691        }
1692    }
1693
1694    #[tokio::test]
1695    async fn resolve_not_pending_keeps_pending_waiters() {
1696        let mut registry = CompletionRegistry::new();
1697        let keep_id = InputId::new();
1698        let drop_id = InputId::new();
1699
1700        let keep_handle = registry.register(keep_id.clone());
1701        let drop_handle = registry.register(drop_id.clone());
1702        registry.resolve_not_pending_runtime_terminated(
1703            |input_id| input_id == &keep_id,
1704            "runtime recycled",
1705            authority(
1706                RuntimeCompletionResultClass::RuntimeTerminated,
1707                RuntimeCompletionObservedOutcome::RuntimeTerminated,
1708            ),
1709        );
1710        assert_eq!(registry.debug_waiter_count(), 1);
1711
1712        match drop_handle.wait_authorized().await {
1713            CompletionOutcome::RuntimeTerminated { reason, .. } => {
1714                assert_eq!(reason, "runtime recycled");
1715            }
1716            other => panic!("Expected RuntimeTerminated, got {other:?}"),
1717        }
1718
1719        registry.resolve_without_result_authorized(
1720            &keep_id,
1721            authority(
1722                RuntimeCompletionResultClass::CompletedWithoutResult,
1723                RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1724            ),
1725        );
1726        match keep_handle.wait_authorized().await {
1727            CompletionOutcome::CompletedWithoutResult => {}
1728            other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1729        }
1730    }
1731
1732    #[tokio::test]
1733    async fn mismatched_not_pending_runtime_terminated_authority_fails_selected_waiters_closed() {
1734        let mut registry = CompletionRegistry::new();
1735        let keep_id = InputId::new();
1736        let drop_id = InputId::new();
1737
1738        let keep_handle = registry.register(keep_id.clone());
1739        let drop_handle = registry.register(drop_id);
1740        registry.resolve_not_pending_runtime_terminated(
1741            |input_id| input_id == &keep_id,
1742            "runtime recycled",
1743            authority(
1744                RuntimeCompletionResultClass::Completed,
1745                RuntimeCompletionObservedOutcome::Completed,
1746            ),
1747        );
1748
1749        assert_eq!(registry.debug_waiter_count(), 1);
1750        match drop_handle.try_wait().await {
1751            Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1752                assert!(reason.contains("Completed"));
1753                assert!(reason.contains("RuntimeTerminated"));
1754            }
1755            other => panic!("Expected authority mismatch wait error, got {other:?}"),
1756        }
1757
1758        registry.resolve_without_result_authorized(
1759            &keep_id,
1760            authority(
1761                RuntimeCompletionResultClass::CompletedWithoutResult,
1762                RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1763            ),
1764        );
1765        match keep_handle.wait_authorized().await {
1766            CompletionOutcome::CompletedWithoutResult => {}
1767            other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1768        }
1769    }
1770
1771    #[tokio::test]
1772    async fn resolve_without_result_nonexistent_is_a_noop() {
1773        let mut registry = CompletionRegistry::new();
1774        registry.resolve_without_result_authorized(
1775            &InputId::new(),
1776            authority(
1777                RuntimeCompletionResultClass::CompletedWithoutResult,
1778                RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1779            ),
1780        );
1781        assert!(!registry.debug_has_waiters());
1782    }
1783
1784    #[test]
1785    fn abandoned_carries_typed_error_metadata() {
1786        let error = TurnErrorMetadata::runtime_apply_failure("apply blew up");
1787        let outcome = CompletionOutcome::Abandoned {
1788            reason: "abandoned".into(),
1789            error: error.clone(),
1790        };
1791        assert_eq!(outcome.abandoned_reason(), Some("abandoned"));
1792        assert_eq!(outcome.error_metadata(), Some(&error));
1793    }
1794
1795    #[test]
1796    fn runtime_terminated_carries_typed_error_metadata() {
1797        let outcome = CompletionOutcome::runtime_terminated("runtime stopped");
1798        match &outcome {
1799            CompletionOutcome::RuntimeTerminated { reason, .. } => {
1800                assert_eq!(reason, "runtime stopped");
1801            }
1802            other => panic!("Expected RuntimeTerminated, got {other:?}"),
1803        }
1804        let metadata = outcome
1805            .error_metadata()
1806            .expect("RuntimeTerminated must carry typed turn error metadata");
1807        assert_eq!(metadata.kind, TurnTerminalCauseKind::FatalFailure);
1808        assert_eq!(metadata.outcome, Some(TurnTerminalOutcome::Failed));
1809        assert!(metadata.terminal);
1810        assert_eq!(metadata.detail.as_deref(), Some("runtime stopped"));
1811    }
1812}