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