From 6c0cdf43e48ea27241c81ad13511c8442d5d7f20 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 04:24:54 +0100 Subject: [PATCH] fix: honor subagent spawn model overrides --- CHANGELOG.md | 1 + ...-tools.subagents.sessions-spawn.model.test.ts | 11 ++++++++++- src/agents/subagent-spawn-plan.ts | 16 ++++++++++++++-- src/agents/subagent-spawn.model-session.test.ts | 1 + src/agents/subagent-spawn.test-helpers.ts | 4 ++++ src/agents/subagent-spawn.test.ts | 1 + src/agents/subagent-spawn.ts | 3 +++ 7 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c019cf05f..96f4647dba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/startup: keep value-option foreground starts on the gateway fast path and skip proxy bootstrap unless proxy env is configured, reducing normal gateway startup RSS and avoiding full CLI graph loading. Thanks @vincentkoc. +- Subagents/models: persist `sessions_spawn.model` and configured subagent models as child-session model overrides before the first turn, so spawned subagents actually run on the requested provider/model instead of reverting to the target agent default. Fixes #73180. Thanks @danielzinhu99. - Backup: skip installed plugin `extensions/*/node_modules` dependency trees while keeping plugin manifests and source files in archives, so local backups avoid rebuildable npm payload bloat. Fixes #64144. Thanks @BrilliantWang. - Cron/models: fail isolated cron runs closed when an explicit `payload.model` is not allowed or cannot be resolved, so scheduled jobs do not silently fall back to an unrelated agent default or paid route before configured provider proxies such as LiteLLM can run. Fixes #73146. Thanks @oneandrewwang. - Memory/QMD: back off repeated chat-turn QMD open failures while still letting memory status and CLI probes recheck immediately, so a broken sidecar dependency cannot trigger active-memory or cron retry storms. Fixes #73188 and #73176. Thanks @leonlushgit and @w3i-William. 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 95032df7696..4bb10bdc02d 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts @@ -4,6 +4,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; import { resolveConfiguredSubagentRunTimeoutSeconds, resolveSubagentModelAndThinkingPlan, + splitModelRef, } from "./subagent-spawn-plan.js"; function createConfig(overrides?: Record): OpenClawConfig { @@ -26,10 +27,18 @@ describe("subagent spawn model + thinking plan", () => { modelApplied: true, initialSessionPatch: { model: "claude-haiku-4-5", + modelOverrideSource: "user", }, }); }); + it("preserves model ids containing slashes", () => { + expect(splitModelRef("openrouter/meta-llama/llama-3.3-70b:free")).toEqual({ + provider: "openrouter", + model: "meta-llama/llama-3.3-70b:free", + }); + }); + it("normalizes thinking overrides into the initial patch", () => { const plan = resolveSubagentModelAndThinkingPlan({ cfg: createConfig(), @@ -69,7 +78,7 @@ describe("subagent spawn model + thinking plan", () => { expect(plan).toMatchObject({ status: "ok", resolvedModel: "minimax/MiniMax-M2.7", - initialSessionPatch: { model: "minimax/MiniMax-M2.7" }, + initialSessionPatch: { model: "minimax/MiniMax-M2.7", modelOverrideSource: "auto" }, }); }); diff --git a/src/agents/subagent-spawn-plan.ts b/src/agents/subagent-spawn-plan.ts index f644bd3a2d9..224ef312728 100644 --- a/src/agents/subagent-spawn-plan.ts +++ b/src/agents/subagent-spawn-plan.ts @@ -11,7 +11,14 @@ export function splitModelRef(ref?: string) { if (!trimmed) { return { provider: undefined, model: undefined }; } - const [provider, model] = trimmed.split("/", 2); + const slash = trimmed.indexOf("/"); + if (slash > 0 && slash < trimmed.length - 1) { + const provider = trimmed.slice(0, slash); + const model = trimmed.slice(slash + 1); + return { provider, model }; + } + const provider = undefined; + const model = trimmed; if (model) { return { provider, model }; } @@ -66,7 +73,12 @@ export function resolveSubagentModelAndThinkingPlan(params: { modelApplied: Boolean(resolvedModel), thinkingOverride: thinkingPlan.thinkingOverride, initialSessionPatch: { - ...(resolvedModel ? { model: resolvedModel } : {}), + ...(resolvedModel + ? { + model: resolvedModel, + modelOverrideSource: params.modelOverride?.trim() ? "user" : "auto", + } + : {}), ...thinkingPlan.initialSessionPatch, }, }; diff --git a/src/agents/subagent-spawn.model-session.test.ts b/src/agents/subagent-spawn.model-session.test.ts index f9b041959df..d55e05becee 100644 --- a/src/agents/subagent-spawn.model-session.test.ts +++ b/src/agents/subagent-spawn.model-session.test.ts @@ -89,6 +89,7 @@ describe("spawnSubagentDirect runtime model persistence", () => { sessionKey: /^agent:main:subagent:/, provider: "openai-codex", model: "gpt-5.4", + overrideSource: "user", }); expect(pruneLegacyStoreKeysMock).toHaveBeenCalledTimes(3); expect(operations.indexOf("store:update")).toBeGreaterThan(-1); diff --git a/src/agents/subagent-spawn.test-helpers.ts b/src/agents/subagent-spawn.test-helpers.ts index 49f45908eb6..addae563ea1 100644 --- a/src/agents/subagent-spawn.test-helpers.ts +++ b/src/agents/subagent-spawn.test-helpers.ts @@ -97,6 +97,7 @@ export function expectPersistedRuntimeModel(params: { sessionKey: string | RegExp; provider: string; model: string; + overrideSource?: "auto" | "user"; }) { const [persistedKey, persistedEntry] = Object.entries(params.persistedStore ?? {})[0] ?? []; if (typeof params.sessionKey === "string") { @@ -107,6 +108,9 @@ export function expectPersistedRuntimeModel(params: { expect(persistedEntry).toMatchObject({ modelProvider: params.provider, model: params.model, + providerOverride: params.provider, + modelOverride: params.model, + ...(params.overrideSource ? { modelOverrideSource: params.overrideSource } : {}), }); } diff --git a/src/agents/subagent-spawn.test.ts b/src/agents/subagent-spawn.test.ts index 9e68678af15..3c7d630761c 100644 --- a/src/agents/subagent-spawn.test.ts +++ b/src/agents/subagent-spawn.test.ts @@ -239,6 +239,7 @@ describe("spawnSubagentDirect seam flow", () => { sessionKey: childSessionKey, provider: "openai-codex", model: "gpt-5.4", + overrideSource: "user", }); expect(operations.indexOf("store:update")).toBeGreaterThan(-1); expect(operations.indexOf("gateway:agent")).toBeGreaterThan( diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 217bf94a266..06985b9fba1 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -241,8 +241,11 @@ function buildDirectChildSessionPatch(patch: Record): Partial