From 08f8aea32e319794727831a042d51b2d10da246d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 03:28:25 +0000 Subject: [PATCH] fix(signal): land #31138 syncMessage presence filtering (@Sid-Qin) Landed from contributor PR #31138 by @Sid-Qin. Co-authored-by: Sid-Qin --- CHANGELOG.md | 1 + .../event-handler.inbound-contract.test.ts | 25 +++++++++++++++++++ src/signal/monitor/event-handler.ts | 8 +++--- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f55b23b..093593fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Signal/Sync message null-handling: treat `syncMessage` presence (including `null`) as sync envelope traffic so replayed sentTranscript payloads cannot bypass loop guards after daemon restart. Landed from contributor PR #31138 by @Sid-Qin. Thanks @Sid-Qin. - Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, prevent inbound preview text from leaking into prompt system events, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #31209, #29610, #30432, #30331, and #29501. Thanks @stakeswky, @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff. - Google Chat/Thread replies: set `messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD` on threaded sends so replies attach to existing threads instead of silently failing thread placement. Landed from contributor PR #30965 by @novan. Thanks @novan. - Mattermost/Private channel policy routing: map Mattermost private channel type `P` to group chat type so `groupPolicy`/`groupAllowFrom` gates apply correctly instead of being treated as open public channels. Landed from contributor PR #30891 by @BlueBirdBack. Thanks @BlueBirdBack. diff --git a/src/signal/monitor/event-handler.inbound-contract.test.ts b/src/signal/monitor/event-handler.inbound-contract.test.ts index c47db1b57..840755236 100644 --- a/src/signal/monitor/event-handler.inbound-contract.test.ts +++ b/src/signal/monitor/event-handler.inbound-contract.test.ts @@ -201,4 +201,29 @@ describe("signal createSignalEventHandler inbound contract", () => { expect(capture.ctx).toBeUndefined(); expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); }); + + it("drops sync envelopes when syncMessage is present but null", async () => { + const handler = createSignalEventHandler( + createBaseSignalEventHandlerDeps({ + cfg: { + messages: { inbound: { debounceMs: 0 } }, + channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } }, + }, + historyLimit: 0, + }), + ); + + await handler( + createSignalReceiveEvent({ + syncMessage: null, + dataMessage: { + message: "replayed sentTranscript envelope", + attachments: [], + }, + }), + ); + + expect(capture.ctx).toBeUndefined(); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index a3e51323b..1c233b6b1 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -438,9 +438,11 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { return; } - // For non-own sync messages (e.g., messages synced from other devices), - // we could process them but for now we skip to be conservative - if (envelope.syncMessage) { + // Filter all sync messages (sentTranscript, readReceipts, etc.). + // signal-cli may set syncMessage to null instead of omitting it, so + // check property existence rather than truthiness to avoid replaying + // the bot's own sent messages on daemon restart. + if ("syncMessage" in envelope) { return; }