BUG: Spawned subagents inherit agent.options stamped at agent creation, not the agent's current model selection
#4077
Replies: 2 comments
|
Verified against v0.1.1-rc.2 source — your diagnosis is correct, and the 43-subagent/429-quota observation makes it the strongest evidence yet for a mechanism that has now been independently reported three times. This is the fifth member of a family I have been tracking; the fix blueprint already exists. Source confirmation
Family (5 reports, same root: creation-time snapshot vs live selection)
Fix blueprint (three tiers, from the earlier analysis)
Blind spot worth checking: continuable children on cold resume — does the resume path re-resolve the route from the live default, or keep the stale persisted header until the first request? If the latter, the resume path needs the same freshness guard (this is exactly what bit #3565). One question for your repro: were the workflow subagents spawned through the in-process provider (continuable) or the fork provider? The fork path re-creates the agent from the live default on resume, which would mask the bug in some scenarios — your 43/43 consistency suggests in-process, but confirming pins the fix surface. |
|
This now has a tested alternative route for users who need to stop silent cross-provider delegation immediately. It does not claim to fix DSH's native spawn provider; it runs stock dsh plugin --profile <你的-profile> add pi2dsh
dsh plugin --profile <你的-profile> add @tintinweb/pi-subagentsAn unpinned child inherits the parent session's last durable real request route, not Real acceptance: stock dsh-TUI, parent request on the official route, Reproduction and evidence: |
Uh oh!
There was an error while loading. Please reload this page.
Spawned subagents inherit
agent.optionsstamped at agent creation, not the agent's current model selection — silent cross-provider divergence after a model switchSummary
An agent's
options(provider/model/maxTokens) are stamped once at session/agent creation and are never rewritten when the user changes the model. Per-request model changes are applied by a separate layer — theagent/requestwaterfall (installModelSelectionindsh-agent) — which only overrides the current agent's own requests using a live per-session selection.Subagents, however, inherit the parent's
options(resolveChildAgentOptionsindsh-subagent) and completely bypass that overrides layer.Net consequence: after the user switches the session model, the parent's live requests use the new model while every subsequently spawned subagent runs the old model — potentially a different provider with real billing/quota impact — with no visible sign in the parent session.
Observed in a real session: the parent session ran 100% of its requests on provider
bitdeer, while all 43 subagents spawned by 6workflowruns ran on provideropencode-go/ modeldeepseek-v4-flash, exhausting that provider's weekly quota (429 GoUsageLimitError). The parent session log contains zero references toopencode-go.Environment
0.1.1-rc.2(dsh-agent,dsh-agent-loop,dsh-subagent,dsh-workflow-worker-thread,dsh-llm-pi-ai)dsh web),llm-pi-aimulti-provider adapterReproduction
Preconditions:
llm-pi-ai.providers, e.g.:opencode-gowith modeldeepseek-v4-flashbitdeerwith modeldeepseek-ai/DeepSeek-V4-Flashopencode-go/deepseek-v4-flash(viaagent-default-modelor the session's/modelselection at creation).Steps:
optionsare stamped withopencode-go/deepseek-v4-flash).bitdeer/deepseek-ai/DeepSeek-V4-Flash(composer/model, or changeagent-default-model).bitdeer(visible in the session'srequest/header).workflowwhoseagent()calls pass noprovider/modeloverride (e.g.agent(prompt, { label, phase })).request/header.Actual result: every spawned subagent's
request/headerconfig is{ "provider": "opencode-go", "model": "deepseek-v4-flash" }— the provider captured at agent creation — while the parent's own concurrent requests are{ "provider": "bitdeer", "model": "deepseek-ai/DeepSeek-V4-Flash" }. In the observed session this silently consumed a third-party weekly quota (429 GoUsageLimitError, "Weekly usage limit reached").Expected result: subagents inherit the agent's current effective model selection (the same provider/model the parent is actually using for its own requests), or the delegation layer exposes an explicit, documented "subagent default model" that cannot silently diverge from what the user sees.
Evidence (from the affected session export)
Parent session jsonl (every request, from the first): config is
bitdeer/deepseek-ai/DeepSeek-V4-Flash.Sub-agent session jsonl (every child, e.g.
subagents/<id>/session.jsonl):{"type":"request/header","data":{"header":{"config":{"provider":"opencode-go","model":"deepseek-v4-flash","maxTokens":384000},...}}}Quota error observed in the same sub-agent:
Parent-session workflow records:
tool-workflow/run-start× 6,tool-workflow/agent-start× 31; the workflow scripts passed only{ label, phase }— noprovider/modeloverrides.Code analysis
agent.optionsis a creation-time snapshot that is never rewritten.dsh-agent-loopconstructs the agent with theoptionspassed at creation (prepare(ownerCtx, id, options, session)→new ReactLoopAgent(loopCtx, id, options, session)→this.options = options). Nothing updatesthis.optionswhen the model is switched.Per-request model is seeded from
options, then overridden by a liveselectionwithout touchingoptions(dsh-agent-loop,ReactLoopAgent.buildRequest):The
agent/requestwaterfall is whereinstallModelSelection(dsh-agent/lib/types/model-selection.js) applies the live per-session selection — so a model switch changes the agent's own requests whilethis.optionsstays frozen at the creation-time value.Subagents inherit
parent.optionsand never see step 2's override (dsh-subagent/lib/types/child-agent.js,resolveChildAgentOptions):The
workflowengine only overrides when the model-facing call explicitly does so (dsh-workflow-worker-thread/lib/index.js,startChild):agentOptionsis built only whenrequest.providerorrequest.modelis present; otherwise the child purely inherits step 3.So the two surfaces — the request-layer
selectionand the delegation-inheritedoptions— are deliberately decoupled for live switching, but only one of them updates, turning the decoupling into a silent misroute for every delegated child. There is no guard or warning when a child's inherited provider differs from the parent's current request provider.Suggested fixes (any one prevents the silent divergence)
selectioninstallModelSelectionapplies to the parent's requests), instead of the rawparent.optionssnapshot./modeloragent-default-model), also updateagent.options— or otherwise version the stamp — so delegation inheritance tracks the switch.Notes / secondary observation
In the affected deployment the base composition default
agent-default-modelisdeepseek-official/deepseek-v4-flash, but nollm-deepseeksection is configured, so that route is unserved; the bare model iddeepseek-v4-flashis offered byopencode-go(a pi-ai built-in provider) while other providers use namespaced ids. This makes a misrouted default land on a specific provider quietly. Consider logging a warning whenever a request's provider is resolved from a bare model id across providers, and/or validating thatagent-default-modelpoints at a configured route at creation.Closing notes
No code patch yet — this is a bug report with reproduction, observed evidence, and code-level analysis. Happy to provide sanitized log excerpts or a minimal repro setup on request.
All reactions