🤖 fix: inherit calling-chat Exec settings in Exec sub-agents - #4080
Conversation
Keep explicit child overrides ahead of the chat's saved Exec choice and global defaults behind it. Preserve raw preference provenance without persisting display-only legacy buckets; cover queueing, nested delegation, reactivation, and handoff. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=26.07 -->
Keep delegated Exec defaults symbolic until launch, preserve explicit Standard/Pro choices independently of effort, and cover the full-app desktop/phone settings flow.
Drive real model picker and send interactions, execute real task tools with a provider-only SDK fake, and check child provider requests plus persisted settings across two Exec selections. The regression intentionally fails on the unfixed baseline: both children use the global fallback.
Explain creation-time resolution and the UI Exec fallback when the calling chat has no saved selection. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=46.66 -->
Remove a stale fixture member that blocks the inherited-Exec branch's pre-push typecheck. Session lookup no longer belongs to Config; pinned-order behavior is unchanged and its tests pass. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=58.46 -->
This comment has been minimized.
This comment has been minimized.
|
@codex security review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 05aa0921d3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The five flat-list useWorkspaceActions overrides dropped the required archivingWorkspaceIds set from the shared fixture, causing row rendering to throw before their assertions. Match the empty-set default provided by WorkspaceContext without changing product behavior. --- _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$24.82`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=24.82 -->
Preserve Standard when older workspace Exec settings omit reasoningMode. Keep explicitly authored Exec subagent fields even when they equal global defaults, while retaining legacy-only mirror cleanup and other agents' sparse normalization. Add configuration round-trip and child-launch regressions for both Codex findings. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=95.18 -->
|
@codex security review |
This comment has been minimized.
This comment has been minimized.
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Merge current main and keep its single archivingWorkspaceIds fixture entry. Both branches independently fixed the same five mocks, and Git's textual merge produced duplicate properties that failed merge-group TypeScript checks. Validation: make static-check-full; 713 targeted tests; full-app Exec inheritance integration. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `coder:openai/gpt-6-astra` • Thinking: `high`_ _Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `high` • Cost: `$130.15`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=high costs=130.15 -->
|
@codex review 🤖 Re-review current head Generated with Generated with |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Make inherited Exec sub-agents use the calling chat's saved Exec model, thinking, and reasoning settings rather than letting global UI Exec defaults override them. Explicit invocation and sub-agent-profile overrides still win. Plan/Explore/custom agents and
kind: "workspace"interactive turns retain their existing resolution rules.Implementation
parent-workspace-execresolver tier, scoped to targetexec+ profilesubagent, between explicit sub-agent defaults and global Exec defaults.aiSettingsqualifies only when the caller explicitly identifies as Exec. Plan-to-Exec handoff uses the transitioning workspace's choice.Validation
make static-checkremains green. Omitted workspace reasoning mode stays Standard, and explicit canonical Exec overrides survive save/reload even when equal to global defaults.make static-checkpassed, including both TypeScript projects, lint, formatting, and import/link/shell/Docker checks.anthropic:claude-fable-5-1, calleropenai:gpt-6-astra, taskmodel: null/thinking: null→ successful childgpt-6-astraprovider request (via Coder routing).Validation prerequisite
The Unit CI job also exposed five flat-sidebar test mocks missing the required
archivingWorkspaceIdsset. The fixtures now supply empty sets (five added lines); all 49 sidebar tests pass, with no sidebar product change.This branch also removes one obsolete
getSessionDirmock member from the existing cross-project pinned-order test fixture. It is not part ofConfiganymore and prevented baseline typechecking. This is a test-only deletion; runtime pinning behavior is unchanged and its four tests pass.Risks and scope
The provenance boundary is deliberate: a missing raw Exec bucket must not be mistaken for an explicit chat preference. Previously persisted synthesized buckets are indistinguishable from genuine saved choices and remain untouched. No config schema, new inheritance enum, or child migration is introduced. Inherited preferences are still normalized against the final model at launch.
Recorded verification
Inherited task and actual Astra child:
Mobile reasoning controls:
Live inheritance:
02-live-spawn.webm
Explicit overrides and reset:
04-overrides-plan.webm
All evidence uses an isolated disposable sandbox, now stopped and removed. Plan-mode Exec spawning was correctly restricted; the handoff branch is covered by deterministic service tests instead of bypassing Plan permissions.
📋 Implementation Plan
The original approved plan follows verbatim. Its planning-time validation status is historical; implementation and verification are summarized above. Metadata write-back removal and the test-only typecheck repair are the documented adjustments.
Exec sub-agents must inherit the calling chat’s Exec selection
1. Outcome, scope, and recommendation
Fix the behavior, not just the label. When Sub-agents → Exec → Model is inherited, an Exec sub-agent should use the Exec model selected in its calling chat. A global UI Exec default is a fallback, not an override of that chat’s choice.
Concrete acceptance case:
anthropic:claude-fable-5-1.openai:gpt-6-astra.Recommended approach: insert one narrowly scoped, per-field calling-workspace Exec candidate into the existing shared resolver; thread it through the existing TaskService resolution seam; update settings to describe dynamic inheritance. Reuse the existing persisted workspace settings and child launch snapshots. No new stored inheritance enum, migration, configuration switch, runtime snapshot transport, or duplicated resolver.
Estimated net product-code change: +80–150 LoC across resolver/types/adapter/service and settings/reasoning controls, excluding tests, stories, and this plan. Backend portion approximately +30–55; UI portion approximately +50–95. Reuse existing helpers to stay near the lower end. This is an estimate, not a line-count target.
This is a planning-only deliverable. Implementation requires Exec mode. Do not change the personal running instance, rewrite existing child history, commit/push, or create a PR as part of this planning request.
2. Verified diagnosis
Baseline: branch
exec-model-1xpw, HEAD2a1b5b267c0133f3d89d023a55d1c2b725d70386, clean working tree during investigation. Line references below are navigation anchors for that revision.src/browser/features/Settings/Sections/TasksSection.tsx:1059–1108leavesagentAiDefaults.exec.subagent.modelStringabsent for inheritance, but displaysagentAiDefaults.exec.modelStringas the inherited concrete model.src/browser/features/ChatInput/index.tsx:664–718saves a workspace/per-agent preference and callsworkspace.updateAgentAISettings;WorkspaceModeAISync.tsx:48–100restores per-agent selections on a mode switch. A saved Exec bucket survives switching to Plan.src/browser/hooks/useSendMessageOptions.ts:31–93builds active model/thinking/reasoning/agent options. The active Plan model is distinct from the chat’s saved Exec selection.src/node/services/turnRequestBuilder.ts:1685andsrc/node/services/tools/task.ts:96–110expose the active turn model asparentRuntimeAiSettings. It is not necessarily the Exec model.src/node/services/taskService.ts:1892–1957builds fallback layers from target-agent bucket, active-agent bucket, and legacyaiSettings, then calls the node resolver withprofile: "subagent".src/common/utils/ai/resolveAgentAiSettings.ts:72–155ordersconfig-subagent→ globalconfig→ definitions/ancestors →parentRuntime→ generic workspace fallbacks. An unset Exec sub-agent model therefore selects global Fable before reaching the calling chat’s GPT-6-astra.src/node/config/index.ts:3198–3205expands a missingaiSettingsByAgentinto both Plan and Exec buckets from legacyaiSettings. This can make a Plan-only workspace appear to have an Exec preference. RawloadConfigOrDefault()workspace entries do not perform that expansion (normalizePersistedWorkspace, lines 654–704). The new candidate must use raw entries.profile: "subagent"resolution is centralized inTaskService.resolveTaskAISettings.kind: "workspace"usesworkspaceTurnManager.ts:1230withprofile: "interactive"; it is a separate contract and must remain unchanged.aiSettingsand task model/thinking fields; queued starts andsendTreeMessagereuse child-owned settings (taskService.ts:2998–3130,3925–3965,5178–5197,10158–10175).Reproduction: a read-only call to the real resolver returned Fable with source
{ tier: "config", agentId: "exec" }. The assertion-based reproduction below exited 1 withAssertionError, actualanthropic:claude-fable-5-1, expectedopenai:gpt-6-astra. This confirms the resolver mismatch, not a live provider/UI end-to-end reproduction.Minimal reproduction command (same executed resolver inputs/assertion)
Existing baseline tests executed:
TasksSection.ui.test.tsx21 passed,TasksSection.test.ts5 passed. No full typecheck, backend fix validation, or live dogfood has been performed during planning.3. Behavioral contract
3.1 Meaning of inheritance
aiSettingsmay supply the new candidate only when persistedagentId(or legacyagentTypeifagentIdis absent) explicitly identifies Exec. Do not default unknown mode to Exec. Do not trust synthesized display metadata or infer provenance from whether Plan/Exec values happen to match.3.2 Exact precedence, independently for each field
For normalized target
execand profilesubagentonly:agentAiDefaults.exec.subagent).agentAiDefaults.exec).Missing/invalid-empty fields continue down the existing field-selection rules; do not treat a partial object as an all-or-nothing override. Model-only or thinking-only overrides must not erase other inherited fields. Keep alias/provider normalization, thinking policies, selected-versus-effective reasoning, and pro capability checks in their existing final resolution stage.
3.3 Timing and continuation
task_send_messagereactivation.createManyuses one existing config snapshot for the whole batch and its saved launch plans; do not reload config per sibling.resolveTaskAISettings(taskService.ts:11599). Treat that as a new Exec settings resolution from the transitioning workspace, not as ordinary same-agent reactivation. Preserve current handoff behavior otherwise.4. Implementation sequence and intermediate gates
Phase A — Lock the reported regression down before product edits
src/common/utils/ai/resolveAgentAiSettings.test.tswith conflicting global and calling-chat models. First reproduce the present fallback behavior against the desired outcome; after introducing the named context input, adapt the fixture to use that input without weakening the expected result.src/node/services/taskService.test.tsnear existing Exec sub-agent defaults coverage (around line 6436): real persisted parent metadata has separate Plan/Exec models, global Exec is a third model, and the Exec task has no model override.createTestHistoryService()whenever history is involved.Gate A: the targeted regression fails specifically because Fable/global wins over GPT-6-astra/chat Exec. Capture the test command and failure. A compiler error or unrelated environment failure is not a valid red result.
Phase B — Add the narrowly scoped resolver input and wire it once
src/common/types/agentAiSettings.tsparentWorkspaceExecSettings?: AgentAiSettingsLayerValuestoResolveAgentAiSettingsInput."parent-workspace-exec"toAiSettingTier; identifyagentId: "exec"for this source.AiSettingTierand update only those that need the new member.src/common/utils/ai/resolveAgentAiSettings.tsprofile === "subagent"and normalized target IDexec. Do not activate merely because an ancestor being visited is named Exec or the implicit reasoning-only fallback reaches Exec.agentAiDefaults.execentry exists; the existing early return for missing config must not suppress the candidate.src/node/services/agentDefinitions/resolveNodeAgentAiSettings.tssrc/node/services/taskService.ts—resolveTaskAISettingsparentWorkspaceIdto this private method’s parameters. Update its three callsites:createMany(~2999),create(~3863), and plan handoff (~11599, useargs.workspaceId). Each already has the ID.findWorkspaceEntry(params.cfg, params.parentWorkspaceId)?.workspace(taskUtils.ts:166–178).params.cfgis already loaded raw config: this is an in-memory lookup with no extra I/O, IPC, or metadata expansion. The entire grouped launch keeps the same config snapshot.raw.aiSettingsByAgent?.execwhen present, regardless of the parent’s active mode or whether Plan and Exec values are identical. Do not require a model/thinking field to be present before passing a partial layer; reasoning-only values must remain valid.agentIdwith an empty fallback, consulting legacyagentTypeonly ifagentIdis absent. Only explicitexecallows raw legacyaiSettings. Do not call a resume helper that defaults missing identity to Exec. Missing raw entry gives no new candidate and does not alter existing missing-parent handling.parentMeta,getAllWorkspaceMetadata(), orgetWorkspaceMetadata(): those may fabricate Exec values from Plan. Never use equality of bucket values as a provenance heuristic.explicit.model, add another config load, or change global defaults. Preserve the existingparentMetafallback path at its current lower priority.taskService.create, and plan handoff inherit the fix through this seam. Keepkind: "workspace"and other interactive consumers untouched.aiSettings/task model snapshot suffices.Defensive programming: use typed optional layer values and explicit scope guards; preserve existing validation of persisted/user-controlled settings. Assertions belong to impossible internal invariants, not missing settings or malformed user config. No startup-fatal checks or silent fallback to a different provider to hide a model error.
Gate B: the original resolver/service regression is green; precedence, Plan isolation, no-config, custom-agent, and lifecycle tests below pass. Inspect the diff for any changed non-Exec resolution order before proceeding.
Phase C — Make settings accurately represent dynamic inheritance
Primary file:
src/browser/features/Settings/Sections/TasksSection.tsx, especiallyrenderExecSubagentDefaultsand itsAiDefaultsControlscall.Inherits from UI Exec: <global model>and the concretely clamped inherited thinking hint with symbolic descriptions. Explain:AiDefaultsControls→ThinkingSelectorControl(src/browser/components/ThinkingSelector/ThinkingSelector.tsx) with optionalmodelCapabilitiesDeferred?: boolean, defaulting false. Enable it only for the Exec sub-agent card with inherited model.THINKING_LEVELSfromsrc/common/types/thinkingand existing generic option labels, preserving explicit selections without model-specific clamping. Permit the saved Pro preference, but do not invent provider-specific Fast capabilities. Explain that actual model capabilities are enforced at launch. Branch capability/label computation explicitly; do not pass a fake or empty model as a sentinel.reasoningModeInherited?: boolean, set on the Exec card fromentry?.reasoningMode === undefined, independently of model/effort inheritance. Pass only the explicit mode or a non-authoritative"standard"control placeholder—notuiExecEntry.reasoningMode. The inherited flag must suppress any claim that Standard/Pro is the effective inherited mode.undefined.subagent.deriveLegacySubagentAiDefaultsProjection(src/common/types/agentAiDefaults.ts:155–174) is called only by config save for downgrade compatibility (src/node/config/index.ts:2072), not by runtime routing. Leave that projection intact; global config cannot serialize a per-chat dynamic value.Gate C: settings UI tests pass, including reset/persistence and dynamic capability behavior. A full-app story and mobile screenshot demonstrate that the longer wording wraps without overflowing and keyboard access remains intact. Avoid copy-literal-only tests; prove selection, omission, reset, and capability behavior.
5. Regression matrix
Use deliberately different supported model IDs for competing layers; never let identical fixture values mask the winning source. Check effective model plus provenance in resolver tests and persisted/launched settings at the service boundary.
parent-workspace-exec.aiSettingssupplies the new tier; raw Plan/unknown caller does not, even if display metadata synthesized an Exec bucket. Explicit non-ExecagentIdmust beat a stale legacyagentType: exec.modelstill outranks configured sub-agent and chat settings.workspaceService.ts:10914,9493–9507,9602–9608), and the child inherits that model.Place pure precedence/policy cases in
resolveAgentAiSettings.test.ts; adapter forwarding inresolveNodeAgentAiSettings.test.ts; lifecycle/launch cases intaskService.test.tsusing existing fixtures. Extendtools/task.test.tsonly if a missing tool-forwarding assertion is necessary—do not duplicate service coverage.For the UI-to-backend boundary, use a full-app
tests/uitest with the existing mock AI router if practical, otherwise an IPC integration test plus recorded live browser verification. UI tests must drive actual picker actions, not mutate local storage or call the tested backend action directly. IPC setup may seed global defaults and disposable metadata. Do not use provider keys for pure routing tests.6. Validation commands and release gates
Run the narrow red test first, then the complete affected files:
Also run colocated ThinkingSelector tests and affected config compatibility tests if those contracts are touched. Run the selected integration file with
TEST_INTEGRATION=1 bun x jest <test-file> --runInBandusing the repository harness. After the final edit, including formatter changes:Use existing Makefile Storybook/Pixel targets for the added/updated full-app story; verify the target names when implementing. Add a full-app
src/browser/stories/App.taskSettings.stories.tsxrather than extending isolated component story coverage alone. Pin desktop and phone Pixel variants, mirror local viewport globals, and ensure breakpoint-dependent play tests run at a forced narrow container width or guard on rendered width.Capture each gate’s exit status directly. Do not infer success from a pipeline’s final filter. Report unrelated baseline/toolchain failures separately and do not make unrelated repository fixes to get a green report.
Final gate: all targeted/static checks and the actual UI-to-child dogfood below pass. No claim of a fixed bug based solely on settings text or a green pure resolver test.
7. Dedicated dogfooding plan and review evidence
Isolated environment
dev-server-sandbox,tests, andagent-browserskills; load CLI usage withagent-browser skills get corebefore browser commands.KEEP_SANDBOX=1 make dev-server-sandbox \ DEV_SERVER_SANDBOX_ARGS="--clean-projects"XUM_ROOTand free backend/Vite ports. Use only a disposable scratch project and sandbox config. Provider seeding may include credentials: never show or upload provider config, cookies, or secrets.agent-browsersession. Read readiness deterministically from server output. Do not kill or modify the user’s normal Xum instance.User-visible scenarios
Artifacts and completion
attach_file. Save a concise redacted manifest mapping scenario, parent/child IDs, configured/expected/actual model, and request evidence.gh ... --attach; do not open a PR or post evidence now.8. Non-goals and rejected shortcuts
9. Advisor review record
Final status: APPROVED with high confidence by advisor
openai:gpt-5.5-pro/xhigh.Review sequence:
findWorkspaceEntry(cfg, parentWorkspaceId), strict legacy identity, symbolic mode inheritance, and deferred capabilities without changing existing callback contracts. Added corresponding regression cases to the plan.All completed investigation reports have been incorporated. Earlier exploratory suggestions to trust expanded parent metadata, use an empty model as a capability sentinel, or widen the reasoning callback for a three-state click cycle are superseded by the reviewed design above.
Approval is for this implementation plan only. The bug has been reproduced at the resolver boundary; implementation, post-fix tests/static checks, and live recorded dogfooding remain to be performed. Pay particular attention during implementation to exhaustive
AiSettingTierconsumers and rawagentId/agentTypenormalization without an Exec default.Generated with
mux• Model:coder:openai/gpt-6-astra• Thinking:high