From cab7a766fb413f7fb77c168b26d0c24390618448 Mon Sep 17 00:00:00 2001 From: Dale Date: Mon, 2 Mar 2026 06:41:48 -0500 Subject: [PATCH] fix(signal): gate ACK reaction on non-empty early body check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before sending the ACK reaction, verify the message has at least one of: non-empty trimmed text, an attachment, or a quote. Without this guard a whitespace-only message (bodyText === '' after trim) would receive an ACK even though it is dropped by the later bodyText guard, giving users a false 'message accepted' signal. Adds two test cases: - whitespace-only message → no ACK - empty text but has attachment → ACK sent Addresses: https://github.com/openclaw/openclaw/pull/31078#discussion_r2871918100 --- .../event-handler.ack-reaction.test.ts | 28 +++++++++++++++++++ .../signal/src/monitor/event-handler.ts | 8 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/extensions/signal/src/monitor/event-handler.ack-reaction.test.ts b/extensions/signal/src/monitor/event-handler.ack-reaction.test.ts index da97e9d8547..af6ef328601 100644 --- a/extensions/signal/src/monitor/event-handler.ack-reaction.test.ts +++ b/extensions/signal/src/monitor/event-handler.ack-reaction.test.ts @@ -186,6 +186,34 @@ describe("Signal ACK reactions", () => { expect(sendReactionSignal).not.toHaveBeenCalled(); }); + it("does NOT send ack when message body is empty (whitespace-only text, no attachment, no quote)", async () => { + const deps = makeDeps(); + const handler = createSignalEventHandler(deps); + await handler( + makeEvent({ + dataMessage: { message: " ", timestamp: 1700000000000 }, + }), + ); + + expect(sendReactionSignal).not.toHaveBeenCalled(); + }); + + it("sends ack when message has no text but has an attachment", async () => { + const deps = makeDeps(); + const handler = createSignalEventHandler(deps); + await handler( + makeEvent({ + dataMessage: { + message: "", + timestamp: 1700000000000, + attachments: [{ id: "att1", contentType: "image/png", size: 1024 }], + }, + }), + ); + + expect(sendReactionSignal).toHaveBeenCalledTimes(1); + }); + it("sends ack BEFORE dispatch", async () => { const callOrder: string[] = []; vi.mocked(sendReactionSignal).mockImplementation(async () => { diff --git a/extensions/signal/src/monitor/event-handler.ts b/extensions/signal/src/monitor/event-handler.ts index 7bb7492c23c..b571e5f50f1 100644 --- a/extensions/signal/src/monitor/event-handler.ts +++ b/extensions/signal/src/monitor/event-handler.ts @@ -702,9 +702,15 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { return; } + // Early body guard: if the message has no text, no attachment, and no quote it will be + // dropped at the bodyText check below — don't send a false ACK in that case. + const hasEarlyBody = Boolean( + messageText || dataMessage.attachments?.length || dataMessage.quote?.text?.trim(), + ); + // Send ACK reaction immediately — before any awaited I/O (attachment fetch, read receipt) // so the user gets instant visual feedback that their message was received. - if (typeof envelope.timestamp === "number") { + if (hasEarlyBody && typeof envelope.timestamp === "number") { maybeSendSignalAckReaction({ cfg: deps.cfg, agentId: route.agentId,