From 4d146ea2f5a4cad7cab8f7649733cd2eeaa45bf7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 15:28:13 +0100 Subject: [PATCH] fix: dedupe message tool replies (#659) (thanks @mickahouan) --- CHANGELOG.md | 1 + src/agents/pi-embedded-subscribe.test.ts | 47 ++++++++++++++++++++++++ src/agents/pi-embedded-subscribe.ts | 4 +- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 399369fde..e9b0a3e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - CLI: `clawdbot sessions` now includes `elev:*` + `usage:*` flags in the table output. - Branding: normalize user-facing “ClawdBot”/“CLAWDBOT” → “Clawdbot” (CLI, status, docs). - Models/Auth: allow MiniMax API configs without `models.providers.minimax.apiKey` (auth profiles / `MINIMAX_API_KEY`). (#656) — thanks @mneves75. +- Agents: avoid duplicate replies when the message tool sends. (#659) — thanks @mickahouan. ## 2026.1.9 diff --git a/src/agents/pi-embedded-subscribe.test.ts b/src/agents/pi-embedded-subscribe.test.ts index c238b2b8b..0a0035716 100644 --- a/src/agents/pi-embedded-subscribe.test.ts +++ b/src/agents/pi-embedded-subscribe.test.ts @@ -456,6 +456,53 @@ describe("subscribeEmbeddedPiSession", () => { expect(subscription.assistantTexts).toEqual(["Hello block"]); }); + it("suppresses message_end block replies when the message tool already sent", () => { + let handler: ((evt: unknown) => void) | undefined; + const session: StubSession = { + subscribe: (fn) => { + handler = fn; + return () => {}; + }, + }; + + const onBlockReply = vi.fn(); + + subscribeEmbeddedPiSession({ + session: session as unknown as Parameters< + typeof subscribeEmbeddedPiSession + >[0]["session"], + runId: "run", + onBlockReply, + blockReplyBreak: "message_end", + }); + + const messageText = "This is the answer."; + + handler?.({ + type: "tool_execution_start", + toolName: "message", + toolCallId: "tool-message-1", + args: { action: "send", to: "+1555", message: messageText }, + }); + + handler?.({ + type: "tool_execution_end", + toolName: "message", + toolCallId: "tool-message-1", + isError: false, + result: "ok", + }); + + const assistantMessage = { + role: "assistant", + content: [{ type: "text", text: messageText }], + } as AssistantMessage; + + handler?.({ type: "message_end", message: assistantMessage }); + + expect(onBlockReply).not.toHaveBeenCalled(); + }); + it("clears block reply state on message_start", () => { let handler: ((evt: unknown) => void) | undefined; const session: StubSession = { diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index 237391328..b8d41040a 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -558,7 +558,9 @@ export function subscribeEmbeddedPiSession(params: { ? (args as Record) : {}; const action = - typeof argsRecord.action === "string" ? argsRecord.action : ""; + typeof argsRecord.action === "string" + ? argsRecord.action.trim() + : ""; // Track send actions: sendMessage/threadReply for Discord/Slack, sessions_send (no action field), // and message/send or message/thread-reply for the generic message tool. const isMessagingSend =