From 94e7a98bf233f6598b3d0ce071b9471eeec193b6 Mon Sep 17 00:00:00 2001 From: Kasper Neist Date: Fri, 9 Jan 2026 22:57:20 +0100 Subject: [PATCH 1/2] feat(signal): add reaction message support - Add SignalReactionMessage type with emoji, targetAuthor, timestamp - Handle reaction messages in monitor (log and skip for now) - Prevents reactions from showing as unknown media --- src/signal/monitor.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/signal/monitor.ts b/src/signal/monitor.ts index b483ddced..cc989d796 100644 --- a/src/signal/monitor.ts +++ b/src/signal/monitor.ts @@ -41,6 +41,15 @@ type SignalEnvelope = { dataMessage?: SignalDataMessage | null; editMessage?: { dataMessage?: SignalDataMessage | null } | null; syncMessage?: unknown; + reactionMessage?: SignalReactionMessage | null; +}; + +type SignalReactionMessage = { + emoji?: string | null; + targetAuthor?: string | null; + targetAuthorUuid?: string | null; + targetSentTimestamp?: number | null; + isRemove?: boolean | null; }; type SignalDataMessage = { @@ -305,6 +314,22 @@ export async function monitorSignalProvider( const envelope = payload?.envelope; if (!envelope) return; if (envelope.syncMessage) return; + + // Handle reaction messages + if (envelope.reactionMessage) { + const reaction = envelope.reactionMessage; + if (reaction.isRemove) return; // Ignore reaction removals + const emoji = reaction.emoji ?? "πŸ‘"; + const sender = resolveSignalSender(envelope); + if (!sender) return; + const senderDisplay = formatSignalSenderDisplay(sender); + const senderName = envelope.sourceName ?? senderDisplay; + logVerbose(`signal reaction: ${emoji} from ${senderName}`); + // Skip processing reactions as messages for now - just log them + // Future: could dispatch as a notification or store for context + return; + } + const dataMessage = envelope.dataMessage ?? envelope.editMessage?.dataMessage; if (!dataMessage) return; From 5bc3d15bba31a1df7887827693821c7926c493cb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 23:39:18 +0100 Subject: [PATCH 2/2] fix: handle signal reactions safely (#616) (thanks @neist) --- CHANGELOG.md | 1 + src/signal/monitor.tool-result.test.ts | 36 ++++++++++++++++++++++++++ src/signal/monitor.ts | 10 +++---- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28590fe3c..fb273a1bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ - Pairing: replies now include sender ids for Discord/Slack/Signal/iMessage/WhatsApp; pairing list labels them explicitly. - Messages: default inbound/outbound prefixes from the routed agent’s `identity.name` when set. (#578) β€” thanks @p6l-richard - Signal: accept UUID-only senders for pairing/allowlists/routing when sourceNumber is missing. (#523) β€” thanks @neist +- Signal: ignore reaction-only messages so they don't surface as unknown media. (#616) β€” thanks @neist - Agent system prompt: avoid automatic self-updates unless explicitly requested. - Onboarding: tighten QuickStart hint copy for configuring later. - Onboarding: set Gemini 3 Pro as the default model for Gemini API key auth. (#489) β€” thanks @jonasjancarik diff --git a/src/signal/monitor.tool-result.test.ts b/src/signal/monitor.tool-result.test.ts index 5a051210d..2b760fee1 100644 --- a/src/signal/monitor.tool-result.test.ts +++ b/src/signal/monitor.tool-result.test.ts @@ -153,6 +153,42 @@ describe("monitorSignalProvider tool results", () => { ); }); + it("ignores reaction-only messages", async () => { + const abortController = new AbortController(); + + streamMock.mockImplementation(async ({ onEvent }) => { + const payload = { + envelope: { + sourceNumber: "+15550001111", + sourceName: "Ada", + timestamp: 1, + reactionMessage: { + emoji: "πŸ‘", + targetAuthor: "+15550002222", + targetSentTimestamp: 2, + }, + }, + }; + await onEvent({ + event: "receive", + data: JSON.stringify(payload), + }); + abortController.abort(); + }); + + await monitorSignalProvider({ + autoStart: false, + baseUrl: "http://127.0.0.1:8080", + abortSignal: abortController.signal, + }); + + await flush(); + + expect(replyMock).not.toHaveBeenCalled(); + expect(sendMock).not.toHaveBeenCalled(); + expect(updateLastRouteMock).not.toHaveBeenCalled(); + }); + it("does not resend pairing code when a request is already pending", async () => { config = { ...config, diff --git a/src/signal/monitor.ts b/src/signal/monitor.ts index cc989d796..b240aeb1b 100644 --- a/src/signal/monitor.ts +++ b/src/signal/monitor.ts @@ -315,11 +315,12 @@ export async function monitorSignalProvider( if (!envelope) return; if (envelope.syncMessage) return; - // Handle reaction messages - if (envelope.reactionMessage) { + const dataMessage = + envelope.dataMessage ?? envelope.editMessage?.dataMessage; + if (envelope.reactionMessage && !dataMessage) { const reaction = envelope.reactionMessage; if (reaction.isRemove) return; // Ignore reaction removals - const emoji = reaction.emoji ?? "πŸ‘"; + const emoji = reaction.emoji ?? "unknown"; const sender = resolveSignalSender(envelope); if (!sender) return; const senderDisplay = formatSignalSenderDisplay(sender); @@ -329,9 +330,6 @@ export async function monitorSignalProvider( // Future: could dispatch as a notification or store for context return; } - - const dataMessage = - envelope.dataMessage ?? envelope.editMessage?.dataMessage; if (!dataMessage) return; const sender = resolveSignalSender(envelope);