diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d865f213e..0086c644e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Subagents: honor `sessions_spawn` with `expectsCompletionMessage: false` by skipping parent completion handoff delivery while still running child cleanup. Fixes #75848. Thanks @alfredjbclaw. - Gateway/logging: keep deferred channel startup logs on the subsystem logger, so Slack, Discord, Telegram, and voice-call startup messages keep timestamped prefixes. Thanks @vincentkoc. +- ACP/Discord: suppress completion announce delivery for inline thread-bound ACP session runs, so Discord thread-bound ACP replies are not delivered twice. Fixes #60780. Thanks @solavrc. - Discord/threads: ignore webhook-authored copies in already-bound Discord session threads even when the webhook id differs, preventing PluralKit proxy copies from creating duplicate turn pressure. Fixes #52005. Thanks @acgh213. - Discord/threads: return the created thread as partial success when the follow-up initial message fails, so agents do not retry thread creation and create empty duplicate threads. Fixes #48450. Thanks @dahifi. - Discord/components: consume every button or select in a non-reusable component message after the first authorized click, so single-use panels cannot fire sibling callbacks. Fixes #54227. Thanks @fujiwarakasei. diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index 2e2986fd8b2..4053115dca6 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -688,6 +688,7 @@ describe("spawnAcpDirect", () => { expect(accepted.childSessionKey).toMatch(/^agent:codex:acp:/); expect(accepted.runId).toBe("run-1"); expect(accepted.mode).toBe("session"); + expect(accepted.inlineDelivery).toBe(true); const patchCall = hoisted.callGatewayMock.mock.calls .map((call: unknown[]) => call[0] as { method?: string; params?: Record }) .find((request) => request.method === "sessions.patch"); diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index 266ea458b46..ad0a57bf9bc 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -145,6 +145,7 @@ type SpawnAcpResultFields = { childSessionKey?: string; runId?: string; mode?: SpawnAcpMode; + inlineDelivery?: boolean; streamLogPath?: string; note?: string; }; @@ -1494,6 +1495,7 @@ export async function spawnAcpDirect( childSessionKey: sessionKey, runId: childRunId, mode: spawnMode, + ...(deliveryPlan.useInlineDelivery ? { inlineDelivery: true } : {}), note: spawnMode === "session" ? ACP_SPAWN_SESSION_ACCEPTED_NOTE : ACP_SPAWN_ACCEPTED_NOTE, }; } diff --git a/src/agents/tools/sessions-spawn-tool.test.ts b/src/agents/tools/sessions-spawn-tool.test.ts index 4e4f1ef8ada..db695e92620 100644 --- a/src/agents/tools/sessions-spawn-tool.test.ts +++ b/src/agents/tools/sessions-spawn-tool.test.ts @@ -471,6 +471,44 @@ describe("sessions_spawn tool", () => { ); }); + it("suppresses completion announces for inline ACP session delivery", async () => { + registerAcpBackendForTest(); + hoisted.spawnAcpDirectMock.mockResolvedValueOnce({ + status: "accepted", + childSessionKey: "agent:codex:acp:1", + runId: "run-acp", + mode: "session", + inlineDelivery: true, + }); + const tool = createSessionsSpawnTool({ + agentSessionKey: "agent:main:main", + agentChannel: "discord", + agentAccountId: "default", + agentTo: "channel:parent-channel", + agentThreadId: "child-thread", + }); + + await tool.execute("call-inline-acp", { + runtime: "acp", + task: "investigate", + agentId: "codex", + thread: true, + mode: "session", + }); + + expect(hoisted.registerSubagentRunMock).toHaveBeenCalledWith( + expect.objectContaining({ + runId: "run-acp", + childSessionKey: "agent:codex:acp:1", + requesterSessionKey: "agent:main:main", + task: "investigate", + cleanup: "keep", + spawnMode: "session", + expectsCompletionMessage: false, + }), + ); + }); + it("rejects ACP runtime calls from sandboxed requester sessions", async () => { registerAcpBackendForTest(); const tool = createSessionsSpawnTool({ diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index ce2636e3b9f..3d5b52bda90 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -334,6 +334,9 @@ export function createSessionsSpawnTool( to: opts?.agentTo, threadId: opts?.agentThreadId, }); + const shouldExpectCompletionMessage = result.inlineDelivery + ? false + : expectsCompletionMessage; try { registerSubagentRun({ runId: childRunId, @@ -345,7 +348,7 @@ export function createSessionsSpawnTool( cleanup: trackedCleanup, label: label || undefined, runTimeoutSeconds, - expectsCompletionMessage, + expectsCompletionMessage: shouldExpectCompletionMessage, spawnMode: trackedSpawnMode, }); } catch (err) {