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