From 27c968fa693c5e76ded2f262ed1738467e4d605a Mon Sep 17 00:00:00 2001 From: hzhang Date: Thu, 9 Apr 2026 08:17:55 +0100 Subject: [PATCH] fix: prevent idle reminder from re-waking dormant discussion channel The moderator bot's own idle reminder message triggered message_received, which saw senderId != currentSpeaker and called wakeFromDormant, immediately undoing the dormant state just entered. Fix: derive the moderator bot's Discord user ID from the token and skip wake-from-dormant when the sender is the moderator bot itself. Co-Authored-By: Claude Sonnet 4.6 --- plugin/core/moderator-discord.ts | 4 ++-- plugin/hooks/message-received.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/plugin/core/moderator-discord.ts b/plugin/core/moderator-discord.ts index 42ca4d5..bf7b378 100644 --- a/plugin/core/moderator-discord.ts +++ b/plugin/core/moderator-discord.ts @@ -2,7 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; type Logger = { info: (m: string) => void; warn: (m: string) => void }; -function userIdFromToken(token: string): string | undefined { +export function userIdFromBotToken(token: string): string | undefined { try { const segment = token.split(".")[0]; const padded = segment + "=".repeat((4 - (segment.length % 4)) % 4); @@ -19,7 +19,7 @@ export function resolveDiscordUserId(api: OpenClawPluginApi, accountId: string): const accounts = (discord.accounts as Record>) || {}; const acct = accounts[accountId]; if (!acct?.token || typeof acct.token !== "string") return undefined; - return userIdFromToken(acct.token); + return userIdFromBotToken(acct.token); } export type ModeratorMessageResult = diff --git a/plugin/hooks/message-received.ts b/plugin/hooks/message-received.ts index 0201cfb..3ea1d4c 100644 --- a/plugin/hooks/message-received.ts +++ b/plugin/hooks/message-received.ts @@ -3,7 +3,7 @@ import type { ChannelStore } from "../core/channel-store.js"; import type { IdentityRegistry } from "../core/identity-registry.js"; import { parseDiscordChannelId } from "./before-model-resolve.js"; import { isDormant, wakeFromDormant, isCurrentSpeaker, hasSpeakers, setSpeakerList, getInitializingChannels, type SpeakerEntry } from "../turn-manager.js"; -import { sendAndDelete, sendModeratorMessage } from "../core/moderator-discord.js"; +import { sendAndDelete, sendModeratorMessage, userIdFromBotToken } from "../core/moderator-discord.js"; import { fetchVisibleChannelBotAccountIds } from "../core/channel-members.js"; import type { InterruptFn } from "./agent-end.js"; @@ -18,6 +18,9 @@ type Deps = { 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 + // from waking dormant channels (idle reminders must not re-trigger the cycle). + const moderatorBotUserId = moderatorBotToken ? userIdFromBotToken(moderatorBotToken) : undefined; api.on("message_received", async (event, ctx) => { try { @@ -125,8 +128,9 @@ export function registerMessageReceivedHook(deps: Deps): void { interruptTailMatch(channelId); api.logger.info(`dirigent: message_received interrupt tail-match channel=${channelId} senderId=${senderId}`); - // Wake from dormant if needed - if (isDormant(channelId) && moderatorBotToken) { + // Wake from dormant if needed — but ignore the moderator bot's own messages + // (e.g. idle reminder) to prevent it from immediately re-waking the channel. + if (isDormant(channelId) && moderatorBotToken && senderId !== moderatorBotUserId) { const first = wakeFromDormant(channelId); if (first) { const msg = `<@${first.discordUserId}>${scheduleIdentifier}`;