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