From 69590de2765e923e88789120dd2f112bb80fdf30 Mon Sep 17 00:00:00 2001 From: Taras Lukavyi Date: Thu, 26 Feb 2026 11:34:25 +0100 Subject: [PATCH] fix: suppress SUBAGENT_SPAWN_ACCEPTED_NOTE for cron isolated sessions The 'do not poll/sleep' note added to sessions_spawn tool results causes cron isolated agents to immediately end their turn, since the note tells them not to wait for subagent results. In cron isolated sessions, the agent turn IS the entire run, so ending early means subagent results are never collected. Fix: detect cron sessions via includes(':cron:') in agentSessionKey and suppress the note, allowing the agent to poll/wait naturally. Note: PR #27330 used startsWith('cron:') which never matches because the session key format is 'agent:main:cron:...' (starts with 'agent:'). Fixes #27308 Fixes #25069 --- ...subagents.sessions-spawn.cron-note.test.ts | 63 +++++++++++++++++++ src/agents/subagent-spawn.ts | 14 ++++- 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts new file mode 100644 index 00000000000..789ff410db6 --- /dev/null +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import "./test-helpers/fast-core-tools.js"; +import { + getCallGatewayMock, + getSessionsSpawnTool, + resetSessionsSpawnConfigOverride, + setupSessionsSpawnGatewayMock, +} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; +import { resetSubagentRegistryForTests } from "./subagent-registry.js"; +import { SUBAGENT_SPAWN_ACCEPTED_NOTE } from "./subagent-spawn.js"; + +const callGatewayMock = getCallGatewayMock(); + +type SpawnResult = { status?: string; note?: string }; + +describe("sessions_spawn: cron isolated session note suppression", () => { + beforeEach(() => { + callGatewayMock.mockReset(); + resetSubagentRegistryForTests(); + resetSessionsSpawnConfigOverride(); + }); + + it("suppresses ACCEPTED_NOTE for cron isolated sessions (mode=run)", async () => { + setupSessionsSpawnGatewayMock({}); + const tool = await getSessionsSpawnTool({ + agentSessionKey: "agent:main:cron:dd871818:run:cf959c9f", + }); + const result = await tool.execute("call-cron-run", { task: "test task", mode: "run" }); + const details = result.details as SpawnResult; + expect(details.note).toBeUndefined(); + expect(details.status).toBe("accepted"); + }); + + it("preserves ACCEPTED_NOTE for regular sessions (mode=run)", async () => { + setupSessionsSpawnGatewayMock({}); + const tool = await getSessionsSpawnTool({ + agentSessionKey: "agent:main:telegram:63448508", + }); + const result = await tool.execute("call-regular-run", { task: "test task", mode: "run" }); + const details = result.details as SpawnResult; + expect(details.note).toBe(SUBAGENT_SPAWN_ACCEPTED_NOTE); + expect(details.status).toBe("accepted"); + }); + + it("suppresses ACCEPTED_NOTE for any agent with :cron: in session key", async () => { + setupSessionsSpawnGatewayMock({}); + // Ensure the check uses includes(":cron:") not startsWith("cron:") + const tool = await getSessionsSpawnTool({ + agentSessionKey: "agent:marian-job-search:cron:a7c7874a:run:abc123", + }); + const result = await tool.execute("call-other-agent-cron", { task: "test task", mode: "run" }); + expect((result.details as SpawnResult).note).toBeUndefined(); + }); + + it("does not suppress note when agentSessionKey is undefined", async () => { + setupSessionsSpawnGatewayMock({}); + const tool = await getSessionsSpawnTool({ + agentSessionKey: undefined, + }); + const result = await tool.execute("call-no-key", { task: "test task", mode: "run" }); + expect((result.details as SpawnResult).note).toBe(SUBAGENT_SPAWN_ACCEPTED_NOTE); + }); +}); diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 37b612145ed..c3773c46ff3 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -524,13 +524,23 @@ export async function spawnSubagentDirect( } } + // Check if we're in a cron isolated session - don't add "do not poll" note + // because cron sessions end immediately after the agent produces a response, + // so the agent needs to wait for subagent results to keep the turn alive. + const isCronSession = ctx.agentSessionKey?.includes(":cron:"); + const note = + spawnMode === "session" + ? SUBAGENT_SPAWN_SESSION_ACCEPTED_NOTE + : isCronSession + ? undefined + : SUBAGENT_SPAWN_ACCEPTED_NOTE; + return { status: "accepted", childSessionKey, runId: childRunId, mode: spawnMode, - note: - spawnMode === "session" ? SUBAGENT_SPAWN_SESSION_ACCEPTED_NOTE : SUBAGENT_SPAWN_ACCEPTED_NOTE, + note, modelApplied: resolvedModel ? modelApplied : undefined, }; }