Skip to main content

agent_engine/
orchestration.rs

1use agent_core::orchestration::{
2    CatalogSnapshot, CompletionGate, DelegationPolicy, WorkerRegistry, WorkerRole, WorkerTerminal,
3    WorkerWritePolicy,
4};
5use agent_core::prompt::QualifiedModelId;
6use serde::Serialize;
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::sync::Mutex;
9
10/// Why a delegation was refused before any worker was built. How many
11/// workers may run (and exist unreconciled) is the delegation policy's job
12/// (`subagent.max_concurrent` / `subagent.max_total`); the tree only adds
13/// what the policy cannot see: how deep workers nest.
14#[derive(Debug, Clone, Eq, PartialEq)]
15pub enum DelegationTreeDenied {
16    /// The new worker would sit deeper than `subagent.max_depth`.
17    DepthLimit { max_depth: u16 },
18    /// The handle is already reserved (a retried registration or spawn).
19    DuplicateWorker,
20    /// The parent worker is gone (finished or cancelled with its subtree).
21    UnknownParent,
22}
23
24impl std::fmt::Display for DelegationTreeDenied {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::DepthLimit { max_depth } => write!(
28                f,
29                "subagent nesting limit reached: workers may nest {max_depth} level(s) deep \
30                 (subagent.max_depth). Do this work directly instead of delegating further."
31            ),
32            Self::DuplicateWorker => write!(f, "this subagent handle is already registered"),
33            Self::UnknownParent => write!(
34                f,
35                "the parent worker of this delegation has already finished or been cancelled"
36            ),
37        }
38    }
39}
40
41#[derive(Debug, Default)]
42struct DelegationTreeState {
43    depth_by_worker: BTreeMap<String, u16>,
44    parent_by_worker: BTreeMap<String, String>,
45    descendants: usize,
46}
47
48/// Session-scoped runtime enforcement shared by every subagent tool path.
49pub struct OrchestrationRuntime {
50    inner: Mutex<Inner>,
51    max_depth: u16,
52    tree: Mutex<DelegationTreeState>,
53}
54struct Inner {
55    registry: WorkerRegistry,
56    handles: HashMap<String, agent_core::orchestration::WorkerHandle>,
57}
58
59#[derive(Clone, Debug, Serialize)]
60pub struct AuthorizedWorkerModel {
61    pub model: QualifiedModelId,
62    pub selection_source: SelectionSource,
63    pub network_attempted: bool,
64    pub policy_digest: String,
65    pub catalog_snapshot_id: String,
66    pub catalog_digest: String,
67    pub correlation_id: String,
68    pub cross_provider_grant_id: Option<String>,
69}
70#[derive(Clone, Copy, Debug, Serialize)]
71#[serde(rename_all = "snake_case")]
72pub enum SelectionSource {
73    ForegroundInheritance,
74    ExplicitRequest,
75}
76impl SelectionSource {
77    pub fn as_str(self) -> &'static str {
78        match self {
79            Self::ForegroundInheritance => "foreground_inheritance",
80            Self::ExplicitRequest => "explicit_request",
81        }
82    }
83}
84#[derive(Clone, Debug, Serialize)]
85pub struct DispatchDenial {
86    pub kind: &'static str,
87    pub code: &'static str,
88    pub requested_model: Option<String>,
89    pub foreground_model: String,
90    pub selection_source: SelectionSource,
91    pub network_attempted: bool,
92    pub remediation: &'static str,
93}
94impl std::fmt::Display for DispatchDenial {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        write!(
97            f,
98            "{}",
99            serde_json::to_string(self).unwrap_or_else(|_| self.code.into())
100        )
101    }
102}
103impl std::error::Error for DispatchDenial {}
104
105/// Canonicalize a runtime foreground model string into its exact qualified
106/// identity. Legacy configs persist bare Anthropic IDs (e.g. `claude-fable-5`)
107/// which the runtime router still deliberately routes to `anthropic`; the
108/// canonical identity is derived from that single routing entry point — never
109/// from substring heuristics. Unroutable values fail closed.
110pub fn canonical_foreground_identity(raw: &str) -> Result<QualifiedModelId, String> {
111    if let Ok(model) = QualifiedModelId::parse(raw) {
112        return Ok(model);
113    }
114    let route = crate::runtime::openai::resolve_route(raw)
115        .ok_or_else(|| format!("foreground model is unresolved: {raw}"))?;
116    QualifiedModelId::parse(format!("{}/{}", route.provider, route.model))
117        .map_err(|e| e.to_string())
118}
119
120/// Validate an exact identity against source-controlled runtime routing/catalog
121/// descriptors before allowing a model-driven session grant. This operation is
122/// credential- and network-blind: availability is checked later by normal
123/// provider execution, while invented identities fail before policy mutation.
124pub fn validate_user_authorizable_model(raw: &str) -> Result<QualifiedModelId, String> {
125    let model = QualifiedModelId::parse(raw)
126        .map_err(|_| "authorization denied: invalid qualified model".to_string())?;
127    let known = if model.provider() == "anthropic" {
128        agent_core::models::KNOWN_MODELS
129            .iter()
130            .any(|(id, _)| *id == model.model())
131    } else if model.provider() == "openai-codex" {
132        crate::runtime::openai::catalog::codex_static_catalog_models()
133            .iter()
134            .any(|entry| entry.runtime_id() == model.as_str())
135    } else if model.provider() == "xai-auth" {
136        crate::runtime::openai::catalog::xai_model(model.model()).is_some()
137    } else if model.provider() == "github-copilot" {
138        crate::runtime::openai::catalog::github_copilot_runtime_model(model.model()).is_some()
139    } else if model.provider() == "google-gemini" {
140        crate::runtime::openai::catalog::google_gemini_model(model.model()).is_some()
141    } else {
142        crate::runtime::openai::registry::providers()
143            .iter()
144            .find(|provider| provider.key == model.provider())
145            .is_some_and(|provider| {
146                provider
147                    .models
148                    .iter()
149                    .any(|(id, _, _)| *id == model.model())
150            })
151    };
152    if !known || crate::runtime::openai::resolve_route(model.as_str()).is_none() {
153        return Err(format!(
154            "authorization denied: '{}' is not a known routable model",
155            model.as_str()
156        ));
157    }
158    Ok(model)
159}
160
161/// Exact source-controlled OpenRouter worker stack. These identities are
162/// trusted local descriptors (not live catalog results or historical logs).
163const OPENROUTER_WORKER_MODELS: &[&str] = &[
164    "openrouter/deepseek/deepseek-v4-pro",
165    "openrouter/moonshotai/kimi-k2.7-code",
166    "openrouter/z-ai/glm-5.2",
167];
168
169/// Exact manifestless worker choices for a foreground provider.
170fn manifestless_worker_choices(foreground: &QualifiedModelId) -> Vec<QualifiedModelId> {
171    let source = if foreground.provider() == "openrouter" {
172        OPENROUTER_WORKER_MODELS
173    } else {
174        &[]
175    };
176    let mut choices: Vec<_> = source
177        .iter()
178        .filter_map(|value| QualifiedModelId::parse(*value).ok())
179        .collect();
180    choices.push(foreground.clone());
181    choices.sort();
182    choices.dedup();
183    choices
184}
185
186impl OrchestrationRuntime {
187    /// Build a snapshot exclusively from runtime-owned routing/catalog descriptors.
188    /// `manifest_references` are deliberately ignored here: manifests may narrow the
189    /// snapshot, but can never create catalog membership. The foreground is admitted
190    /// only after the credential-free runtime router resolves its exact identity.
191    pub fn trusted_catalog(
192        foreground: &QualifiedModelId,
193        _manifest_references: impl IntoIterator<Item = QualifiedModelId>,
194    ) -> Result<CatalogSnapshot, &'static str> {
195        use crate::runtime::openai::catalog;
196        use agent_core::orchestration::CatalogEntry;
197
198        let route = crate::runtime::openai::resolve_route(foreground.as_str())
199            .ok_or("foreground model is unresolved")?;
200        if route.provider != foreground.provider() {
201            return Err("foreground provider did not resolve exactly");
202        }
203
204        let mut descriptors = catalog::codex_static_catalog_models();
205        descriptors.extend(catalog::xai_static_catalog_models());
206        descriptors.extend(catalog::copilot_static_catalog_models());
207        descriptors.extend(catalog::google_gemini_static_catalog_models());
208        let mut entries: Vec<_> = descriptors
209            .into_iter()
210            .filter_map(|descriptor| QualifiedModelId::parse(descriptor.runtime_id()).ok())
211            .map(|model| CatalogEntry {
212                model,
213                available: true,
214                worker_eligible: true,
215            })
216            .collect();
217        entries.extend(
218            manifestless_worker_choices(foreground)
219                .into_iter()
220                .map(|model| CatalogEntry {
221                    model,
222                    available: true,
223                    worker_eligible: true,
224                }),
225        );
226        // `resolve_route` is itself a trusted local provider descriptor. This is
227        // independent of manifest content and permits configured generic/Anthropic
228        // foreground routes whose live catalogs are not fetched during startup.
229        entries.push(CatalogEntry {
230            model: foreground.clone(),
231            available: true,
232            worker_eligible: true,
233        });
234        Ok(CatalogSnapshot::from_entries(entries))
235    }
236
237    /// The manifestless baseline sized by the user's `subagent.*` limits.
238    /// Every production install without a prompt-manifest policy uses this.
239    pub fn from_limits(
240        foreground: QualifiedModelId,
241        limits: crate::config::SubagentLimits,
242    ) -> Result<Self, &'static str> {
243        Self::baseline(foreground, limits.max_concurrent, limits.max_total)?
244            .with_max_depth(limits.max_depth)
245    }
246
247    /// Secure manifestless baseline: the exact foreground identity is the only
248    /// worker choice in a deterministic runtime-controlled catalog.
249    pub fn baseline(
250        foreground: QualifiedModelId,
251        concurrent: usize,
252        total: usize,
253    ) -> Result<Self, &'static str> {
254        let choices = manifestless_worker_choices(&foreground);
255        let catalog = Self::trusted_catalog(&foreground, std::iter::empty())?;
256        let mut policy =
257            DelegationPolicy::provider_baseline(foreground, catalog, choices, concurrent, total)?;
258        policy.recycle_reconciled_workers = true;
259        Ok(Self::new(policy))
260    }
261
262    pub fn new(policy: DelegationPolicy) -> Self {
263        Self {
264            inner: Mutex::new(Inner {
265                registry: WorkerRegistry::new(policy),
266                handles: HashMap::new(),
267            }),
268            max_depth: crate::config::SubagentLimits::default().max_depth,
269            tree: Mutex::new(DelegationTreeState::default()),
270        }
271    }
272
273    /// How deep workers may nest (`subagent.max_depth`): 1 = only the
274    /// foreground delegates. Zero would forbid all delegation and is refused.
275    pub fn with_max_depth(mut self, max_depth: u16) -> Result<Self, &'static str> {
276        if max_depth == 0 {
277            return Err("invalid subagent nesting depth");
278        }
279        self.max_depth = max_depth;
280        Ok(self)
281    }
282
283    /// Reserve one tree edge before allocating channels/threads/provider
284    /// runtimes. `parent=None` is a foreground-root child. Only nesting depth
285    /// is bounded here: fan-out and totals are the delegation policy's
286    /// limits, checked right after in the same spawn path. Every denial is
287    /// fail-closed and leaves the tree unchanged.
288    pub fn reserve_delegation(
289        &self,
290        worker_id: &str,
291        parent: Option<&str>,
292    ) -> Result<u16, DelegationTreeDenied> {
293        let mut tree = self.tree.lock().unwrap();
294        if tree.depth_by_worker.contains_key(worker_id) {
295            return Err(DelegationTreeDenied::DuplicateWorker);
296        }
297        let depth = match parent {
298            None => 1,
299            Some(parent_id) => tree
300                .depth_by_worker
301                .get(parent_id)
302                .copied()
303                .ok_or(DelegationTreeDenied::UnknownParent)?
304                .saturating_add(1),
305        };
306        if depth > self.max_depth {
307            return Err(DelegationTreeDenied::DepthLimit {
308                max_depth: self.max_depth,
309            });
310        }
311        let parent_key = parent.unwrap_or("<root>");
312        tree.depth_by_worker.insert(worker_id.to_string(), depth);
313        tree.parent_by_worker
314            .insert(worker_id.to_string(), parent_key.to_string());
315        tree.descendants += 1;
316        Ok(depth)
317    }
318
319    /// Release one reserved tree edge. Descendants are recursively removed
320    /// because a live child cannot outlive its parent lineage. The caller-
321    /// supplied `parent` is intentionally ignored: ownership is taken from the
322    /// reservation recorded under the tree lock, so a stale or hostile caller
323    /// cannot decrement another parent's fanout.
324    pub fn release_delegation(&self, worker_id: &str, _parent: Option<&str>) {
325        let mut tree = self.tree.lock().unwrap();
326        let mut stack = vec![worker_id.to_string()];
327        while let Some(current) = stack.pop() {
328            let children = tree
329                .parent_by_worker
330                .iter()
331                .filter_map(|(child, parent)| (parent == &current).then_some(child.clone()))
332                .collect::<Vec<_>>();
333            stack.extend(children);
334            if tree.depth_by_worker.remove(&current).is_some() {
335                tree.descendants = tree.descendants.saturating_sub(1);
336                tree.parent_by_worker.remove(&current);
337            }
338        }
339    }
340
341    pub fn delegation_descendants(&self) -> usize {
342        self.tree.lock().unwrap().descendants
343    }
344    pub fn preflight(&self, model: &str) -> Result<(), String> {
345        let model = QualifiedModelId::parse(model)
346            .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
347        self.inner
348            .lock()
349            .unwrap()
350            .registry
351            .validate_dispatch(&model)
352            .map_err(|e| format!("delegation denied: {}", e.code()))
353    }
354
355    /// Honors an explicit mid-session user trust grant for one exact worker
356    /// model. Trusted models were never meant to be pinned at session start:
357    /// the live policy and catalog are extended in place while worker
358    /// lifecycle state and concurrency limits are preserved. The grant takes
359    /// effect for the next dispatch decision.
360    pub fn grant_worker_model(&self, model: &str) -> Result<(), String> {
361        let model = QualifiedModelId::parse(model)
362            .map_err(|_| "grant denied: invalid qualified model".to_string())?;
363        self.inner
364            .lock()
365            .unwrap()
366            .registry
367            .grant_worker_model(model)
368            .map_err(|error| format!("grant denied: {error}"))
369    }
370
371    /// Read-only UltraCode authorization snapshot. The exact foreground identity,
372    /// policy authorization, and configured limits are checked under one lock; no
373    /// worker reservation or lifecycle state is created.
374    pub fn ultracode_readiness(&self, model: &str) -> Result<(usize, usize), String> {
375        let model = QualifiedModelId::parse(model)
376            .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
377        let inner = self.inner.lock().unwrap();
378        if inner.registry.foreground_model() != &model {
379            return Err("delegation denied: model is not exact foreground".into());
380        }
381        inner
382            .registry
383            .validate_dispatch(&model)
384            .map_err(|e| format!("delegation denied: {}", e.code()))?;
385        let policy = inner.registry.policy();
386        if policy.max_concurrent_workers == 0 || policy.max_total_workers == 0 {
387            return Err("delegation denied: invalid worker limits".into());
388        }
389        Ok((policy.max_concurrent_workers, policy.max_total_workers))
390    }
391
392    /// Single parse/resolve/catalog/policy/limits decision point used by every
393    /// public spawn path. No credential or provider object is reachable here.
394    pub fn resolve_and_authorize(
395        &self,
396        runtime_handle: &str,
397        requested: Option<&str>,
398    ) -> Result<AuthorizedWorkerModel, DispatchDenial> {
399        let mut inner = self.inner.lock().unwrap();
400        let foreground = inner.registry.foreground_model().clone();
401        let selection_source = if requested.is_some() {
402            SelectionSource::ExplicitRequest
403        } else {
404            SelectionSource::ForegroundInheritance
405        };
406        let model = match requested {
407            Some(value) => QualifiedModelId::parse(value).map_err(|_| DispatchDenial {
408                kind: "dispatch_denied",
409                code: "invalid_qualified_model",
410                requested_model: None,
411                foreground_model: foreground.as_str().into(),
412                selection_source,
413                network_attempted: false,
414                remediation: "Omit model to inherit foreground or select an exact session choice.",
415            })?,
416            None => foreground.clone(),
417        };
418        if inner.handles.contains_key(runtime_handle) {
419            return Err(DispatchDenial {
420                kind: "dispatch_denied",
421                code: "duplicate_runtime_handle",
422                requested_model: Some(model.as_str().into()),
423                foreground_model: foreground.as_str().into(),
424                selection_source,
425                network_attempted: false,
426                remediation: "Use a new worker handle.",
427            });
428        }
429        let policy_digest = inner.registry.policy().digest();
430        let catalog_snapshot_id = inner.registry.policy().catalog_snapshot_id().to_owned();
431        let catalog_digest = inner.registry.policy().catalog_digest().to_owned();
432        let cross_provider_grant_id = inner
433            .registry
434            .policy()
435            .authorize(&model)
436            .ok()
437            .flatten()
438            .map(str::to_owned);
439        let handle = inner
440            .registry
441            .authorize_dispatch(&model, WorkerRole::Implementer, WorkerWritePolicy::ReadOnly)
442            .map_err(|error| DispatchDenial {
443                kind: "dispatch_denied",
444                code: error.code(),
445                requested_model: Some(model.as_str().into()),
446                foreground_model: foreground.as_str().into(),
447                selection_source,
448                network_attempted: false,
449                remediation: match error.typed_code() {
450                    agent_core::orchestration::DispatchFailureCode::ConcurrencyLimit =>
451                        if inner.registry.policy().recycle_reconciled_workers {
452                            "Wait for running workers to finish; collect and reconcile their results. The limit is subagent.max_concurrent in config. Changing model does not release capacity."
453                        } else {
454                            "Wait for running workers to finish; collect and reconcile their results. The prompt manifest's delegation policy sets this limit. Changing model does not release capacity."
455                        },
456                    agent_core::orchestration::DispatchFailureCode::TotalWorkerLimit =>
457                        if inner.registry.policy().recycle_reconciled_workers {
458                            "Collect finished workers with reconciled=true to release outstanding-worker capacity (subagent.max_total in config). Changing model does not release capacity."
459                        } else {
460                            "The explicit delegation policy cumulative worker budget is exhausted. Changing model or collecting workers does not renew it."
461                        },
462                    _ => "Omit model to inherit foreground, select an exact session choice, or trust the model mid-session (favorite it in the models picker).",
463                },
464            })?;
465        inner.handles.insert(runtime_handle.to_owned(), handle);
466        Ok(AuthorizedWorkerModel {
467            model,
468            selection_source,
469            network_attempted: false,
470            policy_digest,
471            catalog_snapshot_id,
472            catalog_digest,
473            correlation_id: runtime_handle.to_owned(),
474            cross_provider_grant_id,
475        })
476    }
477
478    /// Marks the persisted handle Starting immediately before the runtime/thread
479    /// factory is invoked. Starting remains rollback-safe until `mark_running`.
480    pub fn mark_starting(&self, runtime_handle: &str) -> Result<(), String> {
481        let mut inner = self.inner.lock().unwrap();
482        let handle = inner
483            .handles
484            .get(runtime_handle)
485            .cloned()
486            .ok_or_else(|| "worker lifecycle unavailable".to_string())?;
487        inner
488            .registry
489            .mark_starting(&handle)
490            .map_err(|_| "worker lifecycle unavailable".into())
491    }
492    pub fn mark_running(&self, runtime_handle: &str) -> Result<(), String> {
493        let mut inner = self.inner.lock().unwrap();
494        let handle = inner
495            .handles
496            .get(runtime_handle)
497            .cloned()
498            .ok_or_else(|| "worker lifecycle unavailable".to_string())?;
499        inner
500            .registry
501            .mark_running(&handle)
502            .map_err(|_| "worker lifecycle unavailable".into())
503    }
504
505    pub fn rollback(&self, runtime_handle: &str) {
506        let mut inner = self.inner.lock().unwrap();
507        if let Some(handle) = inner.handles.get(runtime_handle).cloned() {
508            let _ = inner.registry.rollback_dispatch(&handle);
509            inner.handles.remove(runtime_handle);
510        }
511    }
512
513    pub fn effective_choices(&self) -> Vec<String> {
514        self.inner
515            .lock()
516            .unwrap()
517            .registry
518            .policy()
519            .effective_choices()
520            .iter()
521            .map(|model| model.as_str().to_owned())
522            .collect()
523    }
524
525    pub fn foreground_model(&self) -> String {
526        self.inner
527            .lock()
528            .unwrap()
529            .registry
530            .foreground_model()
531            .as_str()
532            .to_owned()
533    }
534    pub fn authorize_with_policy(
535        &self,
536        runtime_handle: &str,
537        model: &str,
538        role: WorkerRole,
539        writes: WorkerWritePolicy,
540    ) -> Result<(), String> {
541        let model = QualifiedModelId::parse(model)
542            .map_err(|_| "delegation denied: invalid qualified model".to_string())?;
543        let mut inner = self.inner.lock().unwrap();
544        if inner.handles.contains_key(runtime_handle) {
545            return Err("delegation denied: duplicate runtime handle".to_string());
546        }
547        let handle = inner
548            .registry
549            .authorize_dispatch(&model, role, writes)
550            .map_err(|e| format!("delegation denied: {}", e.code()))?;
551        inner.registry.mark_starting(&handle).map_err(|error| {
552            let _ = inner.registry.rollback_dispatch(&handle);
553            error.to_string()
554        })?;
555        if let Err(error) = inner.registry.mark_running(&handle) {
556            let _ = inner.registry.rollback_dispatch(&handle);
557            return Err(error.to_string());
558        }
559        inner.handles.insert(runtime_handle.to_string(), handle);
560        Ok(())
561    }
562    pub fn authorize(&self, runtime_handle: &str, model: &str) -> Result<(), String> {
563        self.authorize_with_policy(
564            runtime_handle,
565            model,
566            WorkerRole::Implementer,
567            WorkerWritePolicy::ReadOnly,
568        )
569    }
570    pub fn poll(&self, id: &str, fingerprint: &str) -> Result<(), String> {
571        let mut inner = self.inner.lock().unwrap();
572        let h = inner
573            .handles
574            .get(id)
575            .cloned()
576            .ok_or_else(|| "unknown worker".to_string())?;
577        inner.registry.poll(&h, fingerprint).map_err(str::to_string)
578    }
579    pub fn steer(&self, id: &str) -> Result<(), String> {
580        let mut inner = self.inner.lock().unwrap();
581        let h = inner
582            .handles
583            .get(id)
584            .cloned()
585            .ok_or_else(|| "unknown worker".to_string())?;
586        inner.registry.steer(&h).map_err(str::to_string)
587    }
588    pub fn terminal_and_collect(&self, id: &str, terminal: WorkerTerminal) -> Result<(), String> {
589        let mut inner = self.inner.lock().unwrap();
590        let h = inner
591            .handles
592            .get(id)
593            .cloned()
594            .ok_or_else(|| "unknown worker".to_string())?;
595        inner
596            .registry
597            .mark_terminal(&h, terminal)
598            .map_err(str::to_string)?;
599        inner.registry.collect(&h).map_err(str::to_string)
600    }
601    pub fn finish_one_shot(&self, id: &str, terminal: WorkerTerminal) -> Result<(), String> {
602        self.terminal_and_collect(id, terminal)?;
603        self.reconcile(id)?;
604        // One-shot results have no retained reactive handle to be reaped later.
605        self.retire_reconciled(&[id.to_owned()].into_iter().collect());
606        Ok(())
607    }
608    pub fn reconcile(&self, id: &str) -> Result<(), String> {
609        let mut inner = self.inner.lock().unwrap();
610        let h = inner
611            .handles
612            .get(id)
613            .cloned()
614            .ok_or_else(|| "unknown worker".to_string())?;
615        inner.registry.reconcile(&h).map_err(str::to_string)
616    }
617    /// Reaper handoff: use exact IDs removed under the runtime registry lock,
618    /// never a complement snapshot that could include newly registered workers.
619    pub fn retire_reconciled(&self, removed_handles: &HashSet<String>) {
620        let mut inner = self.inner.lock().unwrap();
621        for id in removed_handles {
622            if let Some(handle) = inner.handles.get(id).cloned() {
623                if inner.registry.retire_reconciled(&handle) {
624                    inner.handles.remove(id);
625                }
626            }
627        }
628    }
629
630    /// Snapshot runtime handle IDs still named by the completion gate. Taking this
631    /// snapshot before locking the subagent registry avoids cross-registry lock order.
632    pub fn unreconciled_runtime_handles(&self) -> HashSet<String> {
633        let inner = self.inner.lock().unwrap();
634        // Use all_unreconciled_ids (includes Running workers) — NOT
635        // completion_gate (which only reports Terminal|Collected). The reaper
636        // must retain handles the orchestration still tracks, even if the
637        // gate lets the foreground turn through.
638        let unreconciled = inner.registry.all_unreconciled_ids();
639        if unreconciled.is_empty() {
640            return HashSet::new();
641        }
642        inner
643            .handles
644            .iter()
645            .filter(|(_, handle)| unreconciled.iter().any(|id| id == handle.id()))
646            .map(|(runtime_id, _)| runtime_id.clone())
647            .collect()
648    }
649
650    /// Whether a runtime handle is still named by the policy completion gate.
651    pub fn is_unreconciled(&self, runtime_handle: &str) -> bool {
652        self.unreconciled_runtime_handles().contains(runtime_handle)
653    }
654
655    pub fn completion_gate(&self) -> CompletionGate {
656        // Core WorkerRegistry reports internal policy IDs (`worker-N`). Map them
657        // back through the runtime handle table so tool-facing remediation can
658        // cite `sa_*` handles accepted by `subagent_collect`.
659        let inner = self.inner.lock().unwrap();
660        match inner.registry.completion_gate() {
661            CompletionGate::Allowed => CompletionGate::Allowed,
662            CompletionGate::Warning { workers } => CompletionGate::Warning {
663                workers: Self::map_policy_ids_to_runtime(&inner.handles, workers),
664            },
665            CompletionGate::Blocked { workers } => CompletionGate::Blocked {
666                workers: Self::map_policy_ids_to_runtime(&inner.handles, workers),
667            },
668        }
669    }
670
671    /// Map policy `WorkerHandle` IDs (`worker-N`) to runtime handles (`sa_*`).
672    /// Preserves the deterministic order of the policy gate's worker list.
673    fn map_policy_ids_to_runtime(
674        handles: &HashMap<String, agent_core::orchestration::WorkerHandle>,
675        policy_ids: Vec<String>,
676    ) -> Vec<String> {
677        let reverse: HashMap<&str, &str> = handles
678            .iter()
679            .map(|(runtime_handle, policy_handle)| (policy_handle.id(), runtime_handle.as_str()))
680            .collect();
681        policy_ids
682            .into_iter()
683            .map(|policy_id| {
684                reverse
685                    .get(policy_id.as_str())
686                    .map(|runtime| (*runtime).to_owned())
687                    .unwrap_or(policy_id)
688            })
689            .collect()
690    }
691    pub fn check_foreground_write(&self, path: &str) -> agent_core::orchestration::ScopeDecision {
692        self.inner
693            .lock()
694            .unwrap()
695            .registry
696            .check_foreground_write(path)
697    }
698    pub fn telemetry_json(&self) -> String {
699        serde_json::to_string(self.inner.lock().unwrap().registry.telemetry())
700            .unwrap_or_else(|_| "[]".into())
701    }
702    pub fn enforcement_mode(&self) -> agent_core::orchestration::EnforcementMode {
703        self.inner.lock().unwrap().registry.mode()
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710    use std::sync::Arc;
711    fn model(s: &str) -> QualifiedModelId {
712        QualifiedModelId::parse(s).unwrap()
713    }
714    /// Regression: a legacy config (`model = claude-fable-5`, no provider
715    /// segment) must not brick startup. The router deliberately routes bare
716    /// Claude IDs to `anthropic`, so the baseline foreground identity must be
717    /// the router's canonical `anthropic/<id>` — not a startup Config error.
718    #[test]
719    fn legacy_bare_claude_foreground_canonicalizes_via_router() {
720        let id = canonical_foreground_identity("claude-fable-5").unwrap();
721        assert_eq!(id.as_str(), "anthropic/claude-fable-5");
722        // The canonical identity must satisfy the manifestless baseline.
723        OrchestrationRuntime::baseline(id, 8, 64).unwrap();
724    }
725
726    #[test]
727    fn baseline_openrouter_catalog_authorizes_exact_chinese_worker_stack() {
728        let deepseek = model("openrouter/deepseek/deepseek-v4-pro");
729        let glm = model("openrouter/z-ai/glm-5.2");
730        let kimi = model("openrouter/moonshotai/kimi-k2.7-code");
731        let rt = OrchestrationRuntime::baseline(deepseek.clone(), 3, 8).unwrap();
732
733        assert_eq!(
734            rt.effective_choices(),
735            vec![
736                deepseek.as_str().to_owned(),
737                kimi.as_str().to_owned(),
738                glm.as_str().to_owned(),
739            ]
740        );
741        assert_eq!(
742            rt.resolve_and_authorize("sa_glm", Some(glm.as_str()))
743                .unwrap()
744                .model,
745            glm
746        );
747        assert_eq!(
748            rt.resolve_and_authorize("sa_kimi", Some(kimi.as_str()))
749                .unwrap()
750                .model,
751            kimi
752        );
753    }
754
755    #[test]
756    fn manifestless_non_openrouter_baseline_remains_foreground_only() {
757        let foreground = model("anthropic/claude-fable-5");
758        let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
759        assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
760        let denied = rt
761            .resolve_and_authorize(
762                "sa_openrouter",
763                Some("openrouter/moonshotai/kimi-k2.7-code"),
764            )
765            .unwrap_err();
766        assert!(matches!(
767            denied.code,
768            "provider_not_allowed" | "catalog_model_unknown"
769        ));
770    }
771
772    /// Regression: an explicit mid-session user trust grant must flip an
773    /// exact-model dispatch from `provider_not_allowed` to authorized without
774    /// restarting the session. Trusted models were never meant to be pinned
775    /// at session start.
776    #[test]
777    fn baseline_honors_mid_session_user_grant() {
778        let foreground = model("anthropic/claude-fable-5");
779        let requested = "openai-codex/gpt-5.6-sol";
780        let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
781        assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
782        let denied = rt
783            .resolve_and_authorize("sa_denied", Some(requested))
784            .unwrap_err();
785        assert!(matches!(
786            denied.code,
787            "provider_not_allowed" | "catalog_model_unknown"
788        ));
789        assert!(!denied.network_attempted);
790
791        rt.grant_worker_model(requested).unwrap();
792
793        assert!(rt.effective_choices().contains(&requested.to_owned()));
794        let authorized = rt
795            .resolve_and_authorize("sa_granted", Some(requested))
796            .unwrap();
797        assert_eq!(authorized.model.as_str(), requested);
798        assert_eq!(
799            authorized.cross_provider_grant_id.as_deref(),
800            Some("session-user-grant-openai-codex/gpt-5.6-sol")
801        );
802        assert!(!authorized.network_attempted);
803        rt.rollback("sa_granted");
804        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
805    }
806
807    /// A grant for a malformed identity fails closed and changes nothing.
808    #[test]
809    fn mid_session_grant_rejects_invalid_qualified_model() {
810        let foreground = model("anthropic/claude-fable-5");
811        let rt = OrchestrationRuntime::baseline(foreground.clone(), 3, 8).unwrap();
812        assert!(rt.grant_worker_model("").is_err());
813        assert!(rt.grant_worker_model("not-qualified").is_err());
814        assert_eq!(rt.effective_choices(), vec![foreground.as_str().to_owned()]);
815    }
816
817    #[test]
818    fn qualified_foreground_identity_passes_through_unchanged() {
819        for raw in [
820            "openai-codex/gpt-5.6-sol",
821            "anthropic/claude-fable-5",
822            "openrouter/z-ai/glm-5.1",
823        ] {
824            assert_eq!(canonical_foreground_identity(raw).unwrap().as_str(), raw);
825        }
826    }
827
828    #[test]
829    fn unroutable_foreground_identity_fails_closed() {
830        // Neither a valid qualified ID nor routable: empty model segment
831        // under an unknown provider prefix.
832        assert!(canonical_foreground_identity("foo/").is_err());
833        assert!(canonical_foreground_identity("").is_err());
834    }
835
836    #[test]
837    fn trusted_catalog_rejects_unresolved_foreground_and_ignores_manifest_candidates() {
838        let unresolved = model("invented/foreground");
839        assert!(OrchestrationRuntime::trusted_catalog(
840            &unresolved,
841            [model("anthropic/claude-manifest-injected")]
842        )
843        .is_err());
844
845        let foreground = model("openai-codex/gpt-5.1-codex-mini");
846        let catalog = OrchestrationRuntime::trusted_catalog(
847            &foreground,
848            [model("anthropic/claude-manifest-injected")],
849        )
850        .unwrap();
851        assert!(catalog.contains(&foreground));
852        assert!(!catalog.contains(&model("anthropic/claude-manifest-injected")));
853    }
854
855    #[test]
856    fn authorization_reserves_without_claiming_worker_is_running() {
857        let foreground = model("anthropic/claude-foreground");
858        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
859            foreground.clone(),
860            [foreground.clone()],
861            1,
862            1,
863        ));
864        rt.resolve_and_authorize("sa_reserved", None).unwrap();
865        assert!(rt.poll("sa_reserved", "not-started").is_err());
866        rt.mark_starting("sa_reserved").unwrap();
867        assert!(rt.poll("sa_reserved", "not-running").is_err());
868        rt.rollback("sa_reserved");
869        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
870    }
871
872    #[test]
873    fn one_shot_uses_the_full_lifecycle_before_terminal_collection() {
874        let foreground = model("anthropic/foreground");
875        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
876            foreground.clone(),
877            [foreground],
878            1,
879            1,
880        ));
881        rt.resolve_and_authorize("sa_one_shot", None).unwrap();
882        rt.mark_starting("sa_one_shot").unwrap();
883        rt.mark_running("sa_one_shot").unwrap();
884        rt.finish_one_shot("sa_one_shot", WorkerTerminal::Completed)
885            .unwrap();
886        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
887        let telemetry = rt.telemetry_json();
888        for event in [
889            "worker.dispatch_allowed",
890            "worker.starting",
891            "worker.running",
892            "worker.terminal",
893            "worker.collected",
894            "worker.reconciled",
895        ] {
896            assert!(telemetry.contains(event), "missing {event}: {telemetry}");
897        }
898    }
899
900    #[test]
901    fn authorization_happens_before_a_worker_is_registered() {
902        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
903            model("anthropic/foreground"),
904            [model("anthropic/worker")],
905            1,
906            1,
907        ));
908        assert!(rt.authorize("sa_1", "openrouter/worker").is_err());
909        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
910        rt.authorize("sa_2", "anthropic/worker").unwrap();
911        // Running workers pass through the gate (reactive pattern).
912        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
913        rt.poll("sa_2", "same").unwrap();
914        rt.steer("sa_2").unwrap();
915        rt.terminal_and_collect("sa_2", WorkerTerminal::Completed)
916            .unwrap();
917        // Terminal+Collected: gate blocks until reconciled.
918        assert!(matches!(
919            rt.completion_gate(),
920            CompletionGate::Blocked { .. }
921        ));
922        rt.reconcile("sa_2").unwrap();
923        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
924    }
925
926    /// The tree bounds nesting depth only. Fan-out is the policy's
927    /// concurrency limit (see `fanout_is_bounded_by_policy_concurrency_not_a_tree_cap`).
928    #[test]
929    fn delegation_tree_bounds_depth_and_rejects_duplicate_handles() {
930        let foreground = model("anthropic/foreground");
931        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
932            foreground.clone(),
933            [foreground],
934            8,
935            16,
936        ))
937        .with_max_depth(2)
938        .unwrap();
939
940        assert_eq!(rt.reserve_delegation("a", None), Ok(1));
941        assert_eq!(
942            rt.reserve_delegation("a", None),
943            Err(DelegationTreeDenied::DuplicateWorker),
944            "registration/spawn retry cannot double-reserve one handle"
945        );
946        assert_eq!(rt.reserve_delegation("b", Some("a")), Ok(2));
947        assert_eq!(
948            rt.reserve_delegation("too-deep", Some("b")),
949            Err(DelegationTreeDenied::DepthLimit { max_depth: 2 })
950        );
951        // No per-parent child cap: siblings are limited by the policy only.
952        for i in 0..20 {
953            assert_eq!(
954                rt.reserve_delegation(&format!("sibling-{i}"), Some("a")),
955                Ok(2)
956            );
957        }
958        assert_eq!(rt.delegation_descendants(), 22);
959        rt.release_delegation("a", None);
960        assert_eq!(
961            rt.delegation_descendants(),
962            0,
963            "a parent takes its subtree with it"
964        );
965        assert!(OrchestrationRuntime::new(DelegationPolicy::enforced(
966            model("anthropic/foreground"),
967            [model("anthropic/foreground")],
968            1,
969            1,
970        ))
971        .with_max_depth(0)
972        .is_err());
973    }
974
975    #[test]
976    fn depth_denial_names_the_config_key() {
977        let message = DelegationTreeDenied::DepthLimit { max_depth: 3 }.to_string();
978        assert!(message.contains("3 level"), "{message}");
979        assert!(message.contains("subagent.max_depth"), "{message}");
980        assert!(
981            !message.contains("DepthLimit"),
982            "no Debug names in user text: {message}"
983        );
984    }
985
986    /// Active reservation count is exact across reserve/release races.
987    #[test]
988    fn delegation_release_is_idempotent_under_completion_cancel_race() {
989        let foreground = model("anthropic/foreground");
990        let rt = Arc::new(OrchestrationRuntime::new(DelegationPolicy::enforced(
991            foreground.clone(),
992            [foreground],
993            4,
994            8,
995        )));
996        rt.reserve_delegation("child", None).unwrap();
997        let a = Arc::clone(&rt);
998        let b = Arc::clone(&rt);
999        let t1 = std::thread::spawn(move || a.release_delegation("child", None));
1000        let t2 = std::thread::spawn(move || b.release_delegation("child", None));
1001        t1.join().unwrap();
1002        t2.join().unwrap();
1003        assert_eq!(rt.delegation_descendants(), 0);
1004    }
1005
1006    #[test]
1007    fn parent_exit_releases_entire_live_descendant_subtree() {
1008        let foreground = model("anthropic/foreground");
1009        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1010            foreground.clone(),
1011            [foreground],
1012            8,
1013            16,
1014        ));
1015        rt.reserve_delegation("parent", None).unwrap();
1016        rt.reserve_delegation("child", Some("parent")).unwrap();
1017        rt.reserve_delegation("grandchild", Some("child")).unwrap();
1018        assert_eq!(rt.delegation_descendants(), 3);
1019        rt.release_delegation("parent", None);
1020        assert_eq!(rt.delegation_descendants(), 0);
1021        assert_eq!(
1022            rt.reserve_delegation("orphan", Some("child")),
1023            Err(DelegationTreeDenied::UnknownParent)
1024        );
1025    }
1026
1027    /// Active reservation count is exact when a live worker completes while
1028    /// its parent is cancelled and recursively releases the same subtree.
1029    #[test]
1030    fn parent_cancel_racing_child_completion_releases_every_lease() {
1031        let foreground = model("anthropic/foreground");
1032        let rt = Arc::new(OrchestrationRuntime::new(DelegationPolicy::enforced(
1033            foreground.clone(),
1034            [foreground],
1035            8,
1036            16,
1037        )));
1038        rt.reserve_delegation("parent", None).unwrap();
1039        rt.reserve_delegation("child", Some("parent")).unwrap();
1040        rt.reserve_delegation("grandchild", Some("child")).unwrap();
1041        let cancel = Arc::clone(&rt);
1042        let complete = Arc::clone(&rt);
1043        let t1 = std::thread::spawn(move || cancel.release_delegation("parent", None));
1044        let t2 = std::thread::spawn(move || complete.release_delegation("child", Some("parent")));
1045        t1.join().unwrap();
1046        t2.join().unwrap();
1047
1048        assert_eq!(rt.delegation_descendants(), 0);
1049        assert_eq!(
1050            rt.reserve_delegation("orphan", Some("grandchild")),
1051            Err(DelegationTreeDenied::UnknownParent)
1052        );
1053        assert_eq!(rt.reserve_delegation("replacement", None), Ok(1));
1054        rt.release_delegation("replacement", None);
1055        assert_eq!(rt.delegation_descendants(), 0);
1056    }
1057
1058    /// A child receives tree lineage only, never a new policy/grant mutation:
1059    /// even when the session already has one explicit cross-provider grant,
1060    /// child lineage cannot use that fact to authorize a different identity.
1061    #[test]
1062    fn descendant_lineage_cannot_mint_or_broaden_session_model_grants() {
1063        let foreground = model("anthropic/foreground");
1064        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1065            foreground.clone(),
1066            [foreground],
1067            4,
1068            8,
1069        ));
1070        rt.grant_worker_model("openai-codex/gpt-5.6-sol").unwrap();
1071        rt.reserve_delegation("parent", None).unwrap();
1072        rt.reserve_delegation("child", Some("parent")).unwrap();
1073
1074        assert!(rt.preflight("openai-codex/gpt-5.6-sol").is_ok());
1075        assert!(
1076            rt.preflight("openai-codex/gpt-5.6-luna").is_err(),
1077            "a child lineage must not broaden one exact session grant to a sibling model"
1078        );
1079        assert!(
1080            rt.preflight("openrouter/ungranted").is_err(),
1081            "tree lineage must never mint a cross-provider grant"
1082        );
1083        rt.release_delegation("parent", None);
1084        assert_eq!(rt.delegation_descendants(), 0);
1085    }
1086
1087    /// Completion remediation must cite tool-facing `sa_*` handles, never the
1088    /// internal policy IDs (`worker-N`) that `WorkerRegistry` allocates.
1089    #[test]
1090    fn completion_gate_reports_runtime_handles_not_policy_ids() {
1091        let rt = OrchestrationRuntime::new(DelegationPolicy::enforced(
1092            model("anthropic/foreground"),
1093            [model("anthropic/worker")],
1094            2,
1095            2,
1096        ));
1097        rt.authorize("sa_alpha", "anthropic/worker").unwrap();
1098        rt.authorize("sa_beta", "anthropic/worker").unwrap();
1099        // Transition both to Terminal so the gate reports them.
1100        rt.terminal_and_collect("sa_alpha", WorkerTerminal::Completed)
1101            .unwrap();
1102        rt.terminal_and_collect("sa_beta", WorkerTerminal::Completed)
1103            .unwrap();
1104
1105        match rt.completion_gate() {
1106            CompletionGate::Blocked { workers } => {
1107                assert_eq!(
1108                    workers,
1109                    vec!["sa_alpha".to_string(), "sa_beta".to_string()],
1110                    "blocked IDs must be runtime handles in deterministic policy order"
1111                );
1112                for id in &workers {
1113                    assert!(
1114                        id.starts_with("sa_"),
1115                        "blocked id must be a subagent_collect handle, got {id}"
1116                    );
1117                    assert!(
1118                        !id.starts_with("worker-"),
1119                        "blocked id must never leak policy WorkerHandle, got {id}"
1120                    );
1121                }
1122            }
1123            other => panic!("expected Blocked with two sa_* handles, got {other:?}"),
1124        }
1125
1126        // After one reconcile, only the remaining runtime handle is reported.
1127        rt.reconcile("sa_alpha").unwrap();
1128        match rt.completion_gate() {
1129            CompletionGate::Blocked { workers } => {
1130                assert_eq!(workers, vec!["sa_beta".to_string()]);
1131            }
1132            other => panic!("expected only sa_beta still blocked, got {other:?}"),
1133        }
1134    }
1135}
1136
1137#[cfg(test)]
1138mod capacity_recovery_tests {
1139    use super::*;
1140    fn baseline(concurrent: usize, total: usize) -> OrchestrationRuntime {
1141        OrchestrationRuntime::baseline(
1142            QualifiedModelId::parse("anthropic/claude-fable-5").unwrap(),
1143            concurrent,
1144            total,
1145        )
1146        .unwrap()
1147    }
1148    #[test]
1149    fn manifestless_long_session_reuses_capacity_without_replacing_policy() {
1150        let rt = baseline(8, 64);
1151        let choices = rt.effective_choices();
1152        for n in 0..150 {
1153            let id = format!("sa_{n}");
1154            rt.resolve_and_authorize(&id, None).unwrap();
1155            rt.mark_starting(&id).unwrap();
1156            rt.mark_running(&id).unwrap();
1157            rt.terminal_and_collect(&id, WorkerTerminal::Completed)
1158                .unwrap();
1159            rt.reconcile(&id).unwrap();
1160            rt.reconcile(&id).unwrap();
1161        }
1162        assert_eq!(rt.effective_choices(), choices);
1163        assert_eq!(rt.completion_gate(), CompletionGate::Allowed);
1164    }
1165    #[test]
1166    fn retirement_only_touches_explicit_reconciled_ids() {
1167        let rt = baseline(8, 64);
1168        for id in ["old", "new", "uncollected"] {
1169            rt.authorize(id, &rt.foreground_model()).unwrap();
1170            rt.terminal_and_collect(id, WorkerTerminal::Completed)
1171                .unwrap();
1172        }
1173        rt.reconcile("old").unwrap();
1174        rt.reconcile("new").unwrap();
1175        rt.retire_reconciled(&["old".into(), "uncollected".into()].into_iter().collect());
1176        let inner = rt.inner.lock().unwrap();
1177        assert!(!inner.handles.contains_key("old"));
1178        assert!(inner.handles.contains_key("new"));
1179        assert!(inner.handles.contains_key("uncollected"));
1180        assert_eq!(inner.registry.total_dispatched(), 3);
1181    }
1182
1183    #[test]
1184    fn fanout_is_bounded_by_policy_concurrency_not_a_tree_cap() {
1185        let limits = crate::config::SubagentLimits {
1186            max_concurrent: 16,
1187            max_total: 64,
1188            max_depth: 4,
1189        };
1190        let rt = OrchestrationRuntime::from_limits(
1191            QualifiedModelId::parse("anthropic/claude-fable-5").unwrap(),
1192            limits,
1193        )
1194        .unwrap();
1195        assert_eq!(
1196            rt.ultracode_readiness("anthropic/claude-fable-5"),
1197            Ok((16, 64))
1198        );
1199        // Same order as the spawn path: tree reservation, then the policy.
1200        for i in 0..16 {
1201            let id = format!("sa_{i}");
1202            assert_eq!(rt.reserve_delegation(&id, None), Ok(1), "worker {i}");
1203            rt.resolve_and_authorize(&id, None).unwrap();
1204        }
1205        assert_eq!(
1206            rt.reserve_delegation("sa_16", None),
1207            Ok(1),
1208            "the 17th root child is not refused by the tree"
1209        );
1210        let denial = rt.resolve_and_authorize("sa_16", None).unwrap_err();
1211        assert_eq!(denial.code, "concurrency_limit");
1212        assert!(
1213            denial.remediation.contains("subagent.max_concurrent"),
1214            "{}",
1215            denial.remediation
1216        );
1217        assert!(denial.remediation.contains("Changing model does not"));
1218    }
1219
1220    #[test]
1221    fn capacity_returns_only_after_reconcile_and_concurrency_still_applies() {
1222        let rt = baseline(1, 2);
1223        rt.authorize("a", &rt.foreground_model()).unwrap();
1224        let denial = rt.resolve_and_authorize("b", None).unwrap_err();
1225        assert_eq!(denial.code, "concurrency_limit");
1226        assert!(denial.remediation.contains("Changing model does not"));
1227        rt.terminal_and_collect("a", WorkerTerminal::Failed)
1228            .unwrap();
1229        rt.authorize("b", &rt.foreground_model()).unwrap();
1230        rt.terminal_and_collect("b", WorkerTerminal::TimedOut)
1231            .unwrap();
1232        assert_eq!(
1233            rt.resolve_and_authorize("c", None).unwrap_err().code,
1234            "total_limit"
1235        );
1236        rt.reconcile("a").unwrap();
1237        rt.resolve_and_authorize("c", None).unwrap();
1238        assert!(matches!(
1239            rt.completion_gate(),
1240            CompletionGate::Blocked { .. }
1241        ));
1242    }
1243}