From 9e61af4a165849e174b2cff4d24a616f0b20cffc Mon Sep 17 00:00:00 2001 From: hzhang Date: Thu, 9 Apr 2026 09:02:10 +0100 Subject: [PATCH] fix: concluded discussions suppress turns and send single auto-reply Two bugs in concluded discussion channel handling: 1. before_model_resolve did not check rec.discussion.concluded, so it still initialized the speaker list and ran turn management. Fixed by returning NO_REPLY early for concluded discussions (same as report mode). 2. message_received fired for all agent VM contexts, causing multiple "This discussion is closed" auto-replies per incoming message. Fixed with process-level dedup keyed on channelId:messageId (same pattern as agent_end runId dedup). Also fixed message_id extraction to look in metadata.conversation_info.message_id first. Co-Authored-By: Claude Sonnet 4.6 --- plugin/hooks/before-model-resolve.ts | 6 +++++ plugin/hooks/message-received.ts | 40 +++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/plugin/hooks/before-model-resolve.ts b/plugin/hooks/before-model-resolve.ts index aff57a2..631f69a 100644 --- a/plugin/hooks/before-model-resolve.ts +++ b/plugin/hooks/before-model-resolve.ts @@ -60,6 +60,12 @@ export function registerBeforeModelResolveHook(deps: Deps): void { // dead mode: suppress all responses if (mode === "report" || mode === "dead" as string) return NO_REPLY; + // concluded discussion: suppress all agent responses (auto-reply handled by message_received) + if (mode === "discussion") { + const rec = channelStore.getRecord(channelId); + if (rec.discussion?.concluded) return NO_REPLY; + } + // disabled modes: let agents respond freely if (mode === "none" || mode === "work") return; diff --git a/plugin/hooks/message-received.ts b/plugin/hooks/message-received.ts index 3ea1d4c..35540b7 100644 --- a/plugin/hooks/message-received.ts +++ b/plugin/hooks/message-received.ts @@ -16,6 +16,18 @@ type Deps = { interruptTailMatch: InterruptFn; }; +/** + * Process-level dedup for concluded-discussion auto-replies. + * Multiple agent VM contexts all fire message_received for the same incoming message; + * only the first should send the "This discussion is closed" reply. + * Keyed on channelId:messageId; evicted after 500 entries. + */ +const _CONCLUDED_REPLY_DEDUP_KEY = "_dirigentConcludedReplyDedup"; +if (!(globalThis as Record)[_CONCLUDED_REPLY_DEDUP_KEY]) { + (globalThis as Record)[_CONCLUDED_REPLY_DEDUP_KEY] = new Set(); +} +const concludedReplyDedup: Set = (globalThis as Record)[_CONCLUDED_REPLY_DEDUP_KEY] as Set; + export function registerMessageReceivedHook(deps: Deps): void { const { api, channelStore, identityRegistry, moderatorBotToken, scheduleIdentifier, interruptTailMatch } = deps; // Derive the moderator bot's own Discord user ID so we can skip self-messages @@ -61,15 +73,31 @@ export function registerMessageReceivedHook(deps: Deps): void { // but we handle archived auto-reply here) if (mode === "report") return; - // archived: auto-reply via moderator + // archived: auto-reply via moderator (deduped — only one agent instance should reply) if (mode === "discussion") { const rec = channelStore.getRecord(channelId); if (rec.discussion?.concluded && moderatorBotToken) { - await sendModeratorMessage( - moderatorBotToken, channelId, - "This discussion is closed and no longer active.", - api.logger, - ).catch(() => undefined); + const metadata = e.metadata as Record | undefined; + const convInfo = metadata?.conversation_info as Record | undefined; + const incomingMsgId = String( + convInfo?.message_id ?? + metadata?.message_id ?? + metadata?.messageId ?? + e.id ?? "", + ); + const dedupKey = `${channelId}:${incomingMsgId}`; + if (!concludedReplyDedup.has(dedupKey)) { + concludedReplyDedup.add(dedupKey); + if (concludedReplyDedup.size > 500) { + const oldest = concludedReplyDedup.values().next().value; + if (oldest) concludedReplyDedup.delete(oldest); + } + await sendModeratorMessage( + moderatorBotToken, channelId, + "This discussion is closed and no longer active.", + api.logger, + ).catch(() => undefined); + } return; } }