Symptom
Resuming a parked/interrupted sub-agent with the agent tool's followup action fails with a route error that names a provider the caller never selected:
model "<pinned model>" is not served by direct provider deepseek
The child's own route receipt still reports the pinned provider, so the receipt and the executed route disagree.
Reproduction — 3/3 across three different providers
| Profile |
Pinned route (per its own receipt) |
start (cold) |
followup (resume) |
consultant |
openai-codex / gpt-5.6-sol |
reached the Responses API; failed on a rate limit |
failed: not served by direct provider deepseek |
reviewer |
xai / grok-4.6 |
ran normally for many minutes |
failed: not served by direct provider deepseek |
general |
zai / GLM-5.3 |
ran normally |
failed: not served by direct provider deepseek |
Observed: agent_6cc0446e → agent_52b116e6; agent_401e22fc → agent_ec759da8; agent_85eb7e46 → agent_b53d1214. Every resume failed in one step, ~45–70 ms — it never reached a provider.
Control: a cold start with the same reviewer profile, dispatched immediately afterwards, resolved correctly and ran. So the pin is honored on start and dropped on followup.
start with resume_from="<parked id>" also dispatched successfully where followup on the same id failed — a possible workaround, though that is only evidence it got past route resolution, not that it is correct.
Mechanism
The config has top-level provider = "deepseek"; the profiles pin both provider and model. On resume the model pin survives but the provider falls back to the session's active provider. deepseek is a StrictDirect provider, and the resolver rejects a selector matching another provider's offering at crates/config/src/route/resolver.rs:608 (selector_matches_other_provider_offering), producing RouteError::ForeignModelForDirectProvider (crates/config/src/route/errors.rs:53).
Not yet located: the exact line in the resume path where the provider pin is lost. The behavior and the error site are pinned; the drop site is not. Reporting it anyway because the repro is deterministic and cheap.
Why no test catches it
crates/tui/src/tools/subagent/tests.rs:20638 — parked_followup_reuses_successor_and_preserves_route_authority_and_lineage — looks like it covers exactly this, and passes. It asserts:
assert_eq!(spec.child_route.as_ref(), Some(&saved_route));
But its fixture pins provider_id: "deepseek", identical to the stub runtime's default provider. So (a) it asserts the successor's recorded receipt, not the route execution actually uses — and the receipt is the half that works; and (b) with the pinned provider equal to the fallback provider, a fallback is indistinguishable from correct behavior. The fixture masks the exact variable under test.
Why it matters
- The receipt disagrees with the executed route, so any diagnostic built on the receipt is wrong.
- The error names a provider the user never selected, sending debugging in the wrong direction.
- Resume is the documented recovery path for every parked or interrupted child, so this is not an edge case.
Fix direction
Resolve the child's provider from the saved child_route/profile on the resume path with the same authority as start, rather than from the session's active provider. Then make the guard test pin a provider different from the session default and assert the route execution actually uses, not only the receipt.
Related
Same family as #6032 (capability decided by hardcoded tables), #6035 (model pins don't propagate), #6036 (Fleet and agent profile stored twice), #6037 (members over-pin their model).
Symptom
Resuming a parked/interrupted sub-agent with the
agenttool'sfollowupaction fails with a route error that names a provider the caller never selected:The child's own route receipt still reports the pinned provider, so the receipt and the executed route disagree.
Reproduction — 3/3 across three different providers
start(cold)followup(resume)consultantopenai-codex/gpt-5.6-soldeepseekreviewerxai/grok-4.6deepseekgeneralzai/GLM-5.3deepseekObserved:
agent_6cc0446e→agent_52b116e6;agent_401e22fc→agent_ec759da8;agent_85eb7e46→agent_b53d1214. Every resume failed in one step, ~45–70 ms — it never reached a provider.Control: a cold
startwith the samereviewerprofile, dispatched immediately afterwards, resolved correctly and ran. So the pin is honored onstartand dropped onfollowup.startwithresume_from="<parked id>"also dispatched successfully wherefollowupon the same id failed — a possible workaround, though that is only evidence it got past route resolution, not that it is correct.Mechanism
The config has top-level
provider = "deepseek"; the profiles pin both provider and model. On resume the model pin survives but the provider falls back to the session's active provider.deepseekis aStrictDirectprovider, and the resolver rejects a selector matching another provider's offering atcrates/config/src/route/resolver.rs:608(selector_matches_other_provider_offering), producingRouteError::ForeignModelForDirectProvider(crates/config/src/route/errors.rs:53).Not yet located: the exact line in the resume path where the provider pin is lost. The behavior and the error site are pinned; the drop site is not. Reporting it anyway because the repro is deterministic and cheap.
Why no test catches it
crates/tui/src/tools/subagent/tests.rs:20638—parked_followup_reuses_successor_and_preserves_route_authority_and_lineage— looks like it covers exactly this, and passes. It asserts:But its fixture pins
provider_id: "deepseek", identical to the stub runtime's default provider. So (a) it asserts the successor's recorded receipt, not the route execution actually uses — and the receipt is the half that works; and (b) with the pinned provider equal to the fallback provider, a fallback is indistinguishable from correct behavior. The fixture masks the exact variable under test.Why it matters
Fix direction
Resolve the child's provider from the saved
child_route/profile on the resume path with the same authority asstart, rather than from the session's active provider. Then make the guard test pin a provider different from the session default and assert the route execution actually uses, not only the receipt.Related
Same family as #6032 (capability decided by hardcoded tables), #6035 (model pins don't propagate), #6036 (Fleet and agent profile stored twice), #6037 (members over-pin their model).