From 2b04cedfb131d1e998b2b225a66ef86f2b6660e0 Mon Sep 17 00:00:00 2001 From: Josh Avant <830519+joshavant@users.noreply.github.com> Date: Thu, 14 May 2026 05:19:41 -0500 Subject: [PATCH] Fix subagent default model precedence (#81783) * fix subagent default model precedence * docs changelog for subagent default fix --- CHANGELOG.md | 1 + src/agents/model-selection.test.ts | 33 +++++++++++++++++-- src/agents/model-selection.ts | 4 +-- ...ols.subagents.sessions-spawn.model.test.ts | 23 +++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a51be1607a4..6da7d759328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai - macOS/Gateway: fail managed LaunchAgent stop and restart when the configured gateway port remains busy after cleanup instead of reporting success while a listener survives. Fixes #73132. Thanks @BunsDev. - Telegram: ship the isolated polling worker at the root dist path used by the bundled worker loader, avoiding startup failures looking for `dist/telegram-ingress-worker.runtime.js`. - Security/sandbox: include Windows `USERPROFILE` in the sandbox blocked home roots so credential-bearing binds (such as `.codex`, `.openclaw`, or `.ssh` under the Windows user profile) are denied even when `HOME` points at a different shell home. (#63074) Thanks @luoyanglang. +- Agents/subagents: apply `agents.defaults.subagents.model` before target agent primary models during `sessions_spawn`, so model-scoped runtimes such as `claude-cli` stay attached to default child runs. Fixes #81395. (#81783) Thanks @joshavant. - Gateway/OpenAI-compatible HTTP: parse shared JSON endpoint paths without trusting malformed Host headers, avoiding 500s before `/v1/chat/completions`, `/v1/responses`, and `/v1/embeddings` request handling. - Telegram: keep Bot API polling alive during main event-loop stalls by moving ingress to an isolated worker with a durable local spool. Fixes #81132. (#81746) Thanks @joshavant. - Telegram: resolve plugin native commands with the active runtime config so commands like `/codex ...` stay on the native command path. diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index d37ff357908..9da9d44b2e2 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.js"; import { resetLogger, setLoggerOverride } from "../logging/logger.js"; import { createWarnLogCapture } from "../logging/test-helpers/warn-log-capture.js"; +import { resolveAgentHarnessPolicy } from "./harness/policy.js"; import { migrateLegacyRuntimeModelRef } from "./model-runtime-aliases.js"; import { isModelKeyAllowedBySet, providerWildcardModelKey } from "./model-selection-shared.js"; import { @@ -2186,7 +2187,7 @@ describe("normalizeModelSelection", () => { }); describe("resolveSubagentConfiguredModelSelection", () => { - it("prefers the agent primary model over agents.defaults.subagents.model", () => { + it("prefers agents.defaults.subagents.model over the agent primary model", () => { const cfg = { agents: { defaults: { @@ -2203,7 +2204,7 @@ describe("resolveSubagentConfiguredModelSelection", () => { } as OpenClawConfig; expect(resolveSubagentConfiguredModelSelection({ cfg, agentId: "research" })).toBe( - "anthropic/claude-opus-4-6", + "openai/gpt-5.4", ); }); @@ -2228,6 +2229,34 @@ describe("resolveSubagentConfiguredModelSelection", () => { "google/gemini-2.5-pro", ); }); + + it("keeps runtime policy attached to the configured default subagent model", () => { + const cfg = { + agents: { + defaults: { + subagents: { model: "anthropic/claude-sonnet-4-6" }, + models: { + "anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } }, + }, + }, + list: [{ id: "research", model: "anthropic/claude-opus-4-7" }], + }, + } as OpenClawConfig; + + const resolved = resolveSubagentConfiguredModelSelection({ cfg, agentId: "research" }); + + expect(resolved).toBe("anthropic/claude-sonnet-4-6"); + expect( + resolveAgentHarnessPolicy({ + provider: "anthropic", + modelId: "claude-sonnet-4-6", + config: cfg, + }), + ).toEqual({ + runtime: "claude-cli", + runtimeSource: "model", + }); + }); }); describe("resolveSubagentSpawnModelSelection", () => { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 3ea7e98b63e..3766bbb58cd 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -322,8 +322,8 @@ export function resolveSubagentConfiguredModelSelection(params: { const agentConfig = resolveAgentConfig(params.cfg, params.agentId); return ( normalizeModelSelection(agentConfig?.subagents?.model) ?? - normalizeModelSelection(agentConfig?.model) ?? - normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model) + normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model) ?? + normalizeModelSelection(agentConfig?.model) ); } diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts index 6762159f155..9a9be8495eb 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts @@ -121,6 +121,29 @@ describe("subagent spawn model + thinking plan", () => { expect(plan.initialSessionPatch.modelOverrideSource).toBe("auto"); }); + it("prefers default subagent model over target agent primary model", () => { + const cfg = createConfig({ + agents: { + defaults: { subagents: { model: "minimax/MiniMax-M2.7" } }, + list: [{ id: "research", model: { primary: "opencode/claude" } }], + }, + }); + const targetAgentConfig = { + id: "research", + model: { primary: "opencode/claude" }, + }; + const plan = expectOkPlan( + resolveSubagentModelAndThinkingPlan({ + cfg, + targetAgentId: "research", + targetAgentConfig, + }), + ); + expect(plan.resolvedModel).toBe("minimax/MiniMax-M2.7"); + expect(plan.initialSessionPatch.model).toBe("minimax/MiniMax-M2.7"); + expect(plan.initialSessionPatch.modelOverrideSource).toBe("auto"); + }); + it("prefers target agent primary model over global default", () => { const cfg = createConfig({ agents: {