From 9f1f65f0e35db6e13e53f5979d42da491c55c45a Mon Sep 17 00:00:00 2001 From: Shadow Date: Mon, 12 Jan 2026 21:41:47 -0600 Subject: [PATCH] Discord: dedupe listener registration on reload Closes #744 --- CHANGELOG.md | 1 + src/discord/monitor.test.ts | 13 +++++++++++++ src/discord/monitor.ts | 22 +++++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3151e77f6..bc73ced89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm) ### Fixes +- Discord: avoid duplicate message/reaction listeners on monitor reloads. (#744 — thanks @thewilloftheshadow) - System events: include local timestamps when events are injected into prompts. (#245 — thanks @thewilloftheshadow) - Cron: accept `jobId` aliases for cron update/run/remove params in gateway validation. (#252 — thanks @thewilloftheshadow) - Models/Google: normalize Gemini 3 model ids to preview variants before runtime selection. (#795 — thanks @thewilloftheshadow) diff --git a/src/discord/monitor.test.ts b/src/discord/monitor.test.ts index 72b6a4587..81ed3aca0 100644 --- a/src/discord/monitor.test.ts +++ b/src/discord/monitor.test.ts @@ -7,6 +7,7 @@ import { isDiscordGroupAllowedByPolicy, normalizeDiscordAllowList, normalizeDiscordSlug, + registerDiscordListener, resolveDiscordChannelConfig, resolveDiscordGuildEntry, resolveDiscordReplyTarget, @@ -34,6 +35,18 @@ const makeEntries = ( return out; }; +describe("registerDiscordListener", () => { + class FakeListener {} + + it("dedupes listeners by constructor", () => { + const listeners: object[] = []; + + expect(registerDiscordListener(listeners, new FakeListener())).toBe(true); + expect(registerDiscordListener(listeners, new FakeListener())).toBe(false); + expect(listeners).toHaveLength(1); + }); +}); + describe("discord allowlist helpers", () => { it("normalizes slugs", () => { expect(normalizeDiscordSlug("Friends of Clawd")).toBe("friends-of-clawd"); diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 638095f16..9c285b4d1 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -175,6 +175,17 @@ function logSlowDiscordListener(params: { } } +export function registerDiscordListener( + listeners: Array, + listener: object, +) { + if (listeners.some((existing) => existing.constructor === listener.constructor)) { + return false; + } + listeners.push(listener); + return true; +} + async function resolveDiscordThreadStarter(params: { channel: DiscordThreadChannel; client: Client; @@ -602,8 +613,12 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { guildEntries, }); - client.listeners.push(new DiscordMessageListener(messageHandler, logger)); - client.listeners.push( + registerDiscordListener( + client.listeners, + new DiscordMessageListener(messageHandler, logger), + ); + registerDiscordListener( + client.listeners, new DiscordReactionListener({ cfg, accountId: account.accountId, @@ -613,7 +628,8 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { logger, }), ); - client.listeners.push( + registerDiscordListener( + client.listeners, new DiscordReactionRemoveListener({ cfg, accountId: account.accountId,