Skip to main content

meerkat_runtime/
completion.rs

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