From d01e06f09aece29673e18f656adf236eedb5b5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Ahouansou?= Date: Sat, 10 Jan 2026 13:38:28 +0100 Subject: [PATCH 1/2] Fix: dedupe message tool sends --- src/agents/pi-embedded-subscribe.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index 1f2350e11..237391328 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -242,6 +242,16 @@ function extractMessagingToolSend( ? { tool: toolName, provider: "telegram", accountId, to } : undefined; } + if (toolName === "message") { + if (action !== "send" && action !== "thread-reply") return undefined; + const toRaw = typeof args.to === "string" ? args.to : undefined; + if (!toRaw) return undefined; + const providerRaw = + typeof args.provider === "string" ? args.provider.trim() : ""; + const provider = providerRaw || "message"; + const to = toRaw.trim(); + return to ? { tool: toolName, provider, accountId, to } : undefined; + } return undefined; } @@ -310,6 +320,7 @@ export function subscribeEmbeddedPiSession(params: { "whatsapp", "discord", "slack", + "message", "sessions_send", ]); const messagingToolSentTexts: string[] = []; @@ -548,12 +559,15 @@ export function subscribeEmbeddedPiSession(params: { : {}; const action = typeof argsRecord.action === "string" ? argsRecord.action : ""; - // Track send actions: sendMessage/threadReply for Discord/Slack, or sessions_send (no action field) - if ( + // 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 = action === "sendMessage" || action === "threadReply" || - toolName === "sessions_send" - ) { + toolName === "sessions_send" || + (toolName === "message" && + (action === "send" || action === "thread-reply")); + if (isMessagingSend) { const sendTarget = extractMessagingToolSend(toolName, argsRecord); if (sendTarget) { pendingMessagingTargets.set(toolCallId, sendTarget); From 4d146ea2f5a4cad7cab8f7649733cd2eeaa45bf7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 15:28:13 +0100 Subject: [PATCH 2/2] 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 =