Skip to content

Commit 7266af9

Browse files
committed
fix(sessions_spawn): tolerate ACP-only fields for subagent runtime
1 parent 406ae72 commit 7266af9

4 files changed

Lines changed: 40 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
77
### Fixes
88

99
- Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.
10+
- Agents/sessions: let `sessions_spawn runtime="subagent"` ignore ACP-only `streamTo` and `resumeSessionId` fields while keeping ACP passthrough and documenting `streamTo` as ACP-only. Fixes #43556; covers #56326, #61724, #63120, #64714, and #67248. Thanks @skernelx, @damselem, @vvitovec, and @Sanjays2402.
1011
- Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.
1112
- Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.
1213
- Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @codex.

docs/tools/subagents.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ chat channel.
140140
<ParamField path="runtime" type='"subagent" | "acp"' default="subagent">
141141
`acp` is only for external ACP harnesses (`claude`, `droid`, `gemini`, `opencode`, or explicitly requested Codex ACP/acpx) and for `agents.list[]` entries whose `runtime.type` is `acp`.
142142
</ParamField>
143+
<ParamField path="resumeSessionId" type="string">
144+
ACP-only. Resumes an existing ACP harness session when `runtime: "acp"`; ignored for native sub-agent spawns.
145+
</ParamField>
146+
<ParamField path="streamTo" type='"parent"'>
147+
ACP-only. Streams ACP run output to the parent session when `runtime: "acp"`; omit for native sub-agent spawns.
148+
</ParamField>
143149
<ParamField path="model" type="string">
144150
Override the sub-agent model. Invalid values are skipped and the sub-agent runs on the default model with a warning in the tool result.
145151
</ParamField>

src/agents/tools/sessions-spawn-tool.test.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe("sessions_spawn tool", () => {
7474
properties?: {
7575
runtime?: { enum?: string[] };
7676
resumeSessionId?: unknown;
77-
streamTo?: unknown;
77+
streamTo?: { description?: string };
7878
};
7979
};
8080

@@ -94,7 +94,7 @@ describe("sessions_spawn tool", () => {
9494
properties?: {
9595
runtime?: { enum?: string[] };
9696
resumeSessionId?: unknown;
97-
streamTo?: unknown;
97+
streamTo?: { description?: string };
9898
};
9999
};
100100

@@ -103,6 +103,7 @@ describe("sessions_spawn tool", () => {
103103
expect(schema.properties?.runtime?.enum).toEqual(["subagent", "acp"]);
104104
expect(schema.properties?.resumeSessionId).toBeDefined();
105105
expect(schema.properties?.streamTo).toBeDefined();
106+
expect(schema.properties?.streamTo?.description).toContain('Requires runtime="acp"');
106107
});
107108

108109
it("hides ACP runtime affordances when the ACP backend is unhealthy", () => {
@@ -489,7 +490,7 @@ describe("sessions_spawn tool", () => {
489490
);
490491
});
491492

492-
it("rejects resumeSessionId without runtime=acp", async () => {
493+
it("ignores resumeSessionId without runtime=acp", async () => {
493494
const tool = createSessionsSpawnTool({
494495
agentSessionKey: "agent:main:main",
495496
});
@@ -499,8 +500,20 @@ describe("sessions_spawn tool", () => {
499500
resumeSessionId: "7f4a78e0-f6be-43fe-855c-c1c4fd229bc4",
500501
});
501502

502-
expect(JSON.stringify(result)).toContain("resumeSessionId is only supported for runtime=acp");
503-
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
503+
expect(result.details).toMatchObject({
504+
status: "accepted",
505+
childSessionKey: "agent:main:subagent:1",
506+
runId: "run-subagent",
507+
});
508+
expect(hoisted.spawnSubagentDirectMock).toHaveBeenCalledWith(
509+
expect.objectContaining({
510+
task: "resume prior work",
511+
}),
512+
expect.any(Object),
513+
);
514+
expect(hoisted.spawnSubagentDirectMock.mock.calls[0]?.[0]).not.toHaveProperty(
515+
"resumeSessionId",
516+
);
504517
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
505518
});
506519

@@ -529,7 +542,7 @@ describe("sessions_spawn tool", () => {
529542
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
530543
});
531544

532-
it('rejects streamTo when runtime is not "acp"', async () => {
545+
it('ignores streamTo when runtime is not "acp"', async () => {
533546
const tool = createSessionsSpawnTool({
534547
agentSessionKey: "agent:main:main",
535548
});
@@ -541,12 +554,18 @@ describe("sessions_spawn tool", () => {
541554
});
542555

543556
expect(result.details).toMatchObject({
544-
status: "error",
557+
status: "accepted",
558+
childSessionKey: "agent:main:subagent:1",
559+
runId: "run-subagent",
545560
});
546-
const details = result.details as { error?: string };
547-
expect(details.error).toContain("streamTo is only supported for runtime=acp");
548561
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
549-
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
562+
expect(hoisted.spawnSubagentDirectMock).toHaveBeenCalledWith(
563+
expect.objectContaining({
564+
task: "analyze file",
565+
}),
566+
expect.any(Object),
567+
);
568+
expect(hoisted.spawnSubagentDirectMock.mock.calls[0]?.[0]).not.toHaveProperty("streamTo");
550569
});
551570

552571
it("keeps attachment content schema unconstrained for llama.cpp grammar safety", () => {

src/agents/tools/sessions-spawn-tool.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ function createSessionsSpawnToolSchema(params: { acpAvailable: boolean }) {
157157
'Resume an existing agent session by its ID (e.g. a Codex session UUID from ~/.codex/sessions/). Requires runtime="acp". The agent replays conversation history via session/load instead of starting fresh.',
158158
}),
159159
),
160-
streamTo: optionalStringEnum(SESSIONS_SPAWN_ACP_STREAM_TARGETS),
160+
streamTo: optionalStringEnum(SESSIONS_SPAWN_ACP_STREAM_TARGETS, {
161+
description:
162+
'Stream ACP run output to the parent session. Requires runtime="acp"; omit for runtime="subagent".',
163+
}),
161164
}
162165
: {}),
163166
};
@@ -261,22 +264,6 @@ export function createSessionsSpawnTool(
261264
}>)
262265
: undefined;
263266

264-
if (streamTo && runtime !== "acp") {
265-
return jsonResult({
266-
status: "error",
267-
error: `streamTo is only supported for runtime=acp; got runtime=${runtime}`,
268-
...roleContext,
269-
});
270-
}
271-
272-
if (resumeSessionId && runtime !== "acp") {
273-
return jsonResult({
274-
status: "error",
275-
error: `resumeSessionId is only supported for runtime=acp; got runtime=${runtime}`,
276-
...roleContext,
277-
});
278-
}
279-
280267
if (runtime === "acp") {
281268
const { isSpawnAcpAcceptedResult, spawnAcpDirect } = await loadAcpSpawnModule();
282269
if (Array.isArray(attachments) && attachments.length > 0) {

0 commit comments

Comments
 (0)