From b57d36f49c35031120ec298ae98157651989a431 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 23:17:10 +0000 Subject: [PATCH] fix(sessions_spawn): hard-fail invalid model overrides --- CHANGELOG.md | 1 + docs/session-tool.md | 1 + docs/subagents.md | 1 + src/agents/clawdbot-tools.subagents.test.ts | 35 +++++++++++++++++++-- src/agents/tools/sessions-spawn-tool.ts | 32 +++++++++++++------ 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80961d142..e2584d9a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### Fixes - Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect. +- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298. - Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior. - Onboarding: write auth profiles to the multi-agent path (`~/.clawdbot/agents/main/agent/`) so the gateway finds credentials on first startup. Thanks @minghinmatthewlam for PR #327. - Docs: add missing `ui:install` setup step in the README. Thanks @hugobarauna for PR #300. diff --git a/docs/session-tool.md b/docs/session-tool.md index b5a5238e1..1708104d0 100644 --- a/docs/session-tool.md +++ b/docs/session-tool.md @@ -126,6 +126,7 @@ Spawn a sub-agent run in an isolated session and announce the result back to the Parameters: - `task` (required) - `label?` (optional; used for logs/UI) +- `model?` (optional; overrides the sub-agent model; invalid values error) - `timeoutSeconds?` (default 0; 0 = fire-and-forget) - `cleanup?` (`delete|keep`, default `delete`) diff --git a/docs/subagents.md b/docs/subagents.md index 71b805831..939df523e 100644 --- a/docs/subagents.md +++ b/docs/subagents.md @@ -24,6 +24,7 @@ Use `sessions_spawn`: Tool params: - `task` (required) - `label?` (optional) +- `model?` (optional; overrides the sub-agent model; invalid values error) - `timeoutSeconds?` (default `0`; `0` = fire-and-forget) - `cleanup?` (`delete|keep`, default `delete`) diff --git a/src/agents/clawdbot-tools.subagents.test.ts b/src/agents/clawdbot-tools.subagents.test.ts index d89826a06..a4a9e308b 100644 --- a/src/agents/clawdbot-tools.subagents.test.ts +++ b/src/agents/clawdbot-tools.subagents.test.ts @@ -223,9 +223,7 @@ describe("subagents", () => { | undefined; const message = params?.message ?? ""; const reply = - message === "Sub-agent announce step." - ? "ANNOUNCE_SKIP" - : "done"; + message === "Sub-agent announce step." ? "ANNOUNCE_SKIP" : "done"; replyByRunId.set(runId, reply); return { runId, @@ -278,4 +276,35 @@ describe("subagents", () => { model: "claude-haiku-4-5", }); }); + + it("sessions_spawn fails when model override is invalid", async () => { + callGatewayMock.mockReset(); + const calls: Array<{ method?: string; params?: unknown }> = []; + + callGatewayMock.mockImplementation(async (opts: unknown) => { + const request = opts as { method?: string; params?: unknown }; + calls.push(request); + if (request.method === "sessions.patch") { + throw new Error("invalid model: bad-model"); + } + return {}; + }); + + const tool = createClawdbotTools({ + agentSessionKey: "main", + agentProvider: "whatsapp", + }).find((candidate) => candidate.name === "sessions_spawn"); + if (!tool) throw new Error("missing sessions_spawn tool"); + + const result = await tool.execute("call4", { + task: "do thing", + timeoutSeconds: 1, + model: "bad-model", + }); + expect(result.details).toMatchObject({ status: "error" }); + expect( + String((result.details as { error?: string }).error ?? ""), + ).toContain("invalid model"); + expect(calls.some((call) => call.method === "agent")).toBe(false); + }); }); diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index 091772d13..ef8db652e 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -220,26 +220,38 @@ export function createSessionsSpawnTool(opts?: { parseAgentSessionKey(requesterInternalKey)?.agentId, ); const childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`; - const patchParams: { key: string; spawnedBy?: string; model?: string } = { - key: childSessionKey, - }; if (opts?.sandboxed === true) { - patchParams.spawnedBy = requesterInternalKey; - } - if (model) { - patchParams.model = model; - } - if (patchParams.spawnedBy || patchParams.model) { try { await callGateway({ method: "sessions.patch", - params: patchParams, + params: { key: childSessionKey, spawnedBy: requesterInternalKey }, timeoutMs: 10_000, }); } catch { // best-effort; scoping relies on this metadata but spawning still works without it } } + if (model) { + try { + await callGateway({ + method: "sessions.patch", + params: { key: childSessionKey, model }, + timeoutMs: 10_000, + }); + } catch (err) { + const messageText = + err instanceof Error + ? err.message + : typeof err === "string" + ? err + : "error"; + return jsonResult({ + status: "error", + error: messageText, + childSessionKey, + }); + } + } const childSystemPrompt = buildSubagentSystemPrompt({ requesterSessionKey, requesterProvider: opts?.agentProvider,