import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { resolvePolicy, type DirigentConfig } from "../rules.js"; import { onSpeakerDone, setWaitingForHuman } from "../turn-manager.js"; import { extractDiscordChannelId, extractDiscordChannelIdFromSessionKey } from "../channel-resolver.js"; type DebugConfig = { enableDebugLogs?: boolean; debugLogChannelIds?: string[]; }; type MessageSentDeps = { api: OpenClawPluginApi; baseConfig: DirigentConfig; policyState: { channelPolicies: Record }; sessionChannelId: Map; sessionAccountId: Map; sessionTurnHandled: Set; ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void; resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined; sendModeratorMessage: ( botToken: string, channelId: string, content: string, logger: { info: (m: string) => void; warn: (m: string) => void }, ) => Promise; }; export function registerMessageSentHook(deps: MessageSentDeps): void { const { api, baseConfig, policyState, sessionChannelId, sessionAccountId, sessionTurnHandled, ensurePolicyStateLoaded, resolveDiscordUserId, sendModeratorMessage, } = deps; api.on("message_sent", async (event, ctx) => { try { const key = ctx.sessionKey; const c = (ctx || {}) as Record; const e = (event || {}) as Record; api.logger.info( `dirigent: DEBUG message_sent RAW ctxKeys=${JSON.stringify(Object.keys(c))} eventKeys=${JSON.stringify(Object.keys(e))} ` + `ctx.channelId=${String(c.channelId ?? "undefined")} ctx.conversationId=${String(c.conversationId ?? "undefined")} ` + `ctx.accountId=${String(c.accountId ?? "undefined")} event.to=${String(e.to ?? "undefined")} ` + `session=${key ?? "undefined"}`, ); let channelId = extractDiscordChannelId(c, e); if (!channelId && key) { channelId = sessionChannelId.get(key); } if (!channelId && key) { channelId = extractDiscordChannelIdFromSessionKey(key); } const accountId = (ctx.accountId as string | undefined) || (key ? sessionAccountId.get(key) : undefined); const content = (event.content as string) || ""; api.logger.info( `dirigent: DEBUG message_sent RESOLVED session=${key ?? "undefined"} channelId=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} content=${content.slice(0, 100)}`, ); if (!channelId || !accountId) return; const live = baseConfig as DirigentConfig & DebugConfig; ensurePolicyStateLoaded(api, live); const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record); const trimmed = content.trim(); const isNoReply = /^NO$/i.test(trimmed) || /^NO_REPLY$/i.test(trimmed); const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : ""; const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar); const waitId = live.waitIdentifier || "👤"; const hasWaitIdentifier = !!lastChar && lastChar === waitId; // Treat explicit NO/NO_REPLY keywords as no-reply. const wasNoReply = isNoReply; if (key && sessionTurnHandled.has(key)) { sessionTurnHandled.delete(key); api.logger.info( `dirigent: message_sent skipping turn advance (already handled in before_message_write) session=${key} channel=${channelId}`, ); return; } if (hasWaitIdentifier) { setWaitingForHuman(channelId); api.logger.info( `dirigent: message_sent wait-for-human triggered channel=${channelId} from=${accountId}`, ); return; } if (wasNoReply || hasEndSymbol) { const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply); const trigger = wasNoReply ? "no_reply_keyword" : "end_symbol"; const noReplyKeyword = wasNoReply ? (/^NO$/i.test(trimmed) ? "NO" : "NO_REPLY") : ""; const keywordNote = wasNoReply ? ` keyword=${noReplyKeyword}` : ""; api.logger.info( `dirigent: turn onSpeakerDone channel=${channelId} from=${accountId} next=${nextSpeaker ?? "dormant"} trigger=${trigger}${keywordNote}`, ); if (wasNoReply && nextSpeaker && live.moderatorBotToken) { const nextUserId = resolveDiscordUserId(api, nextSpeaker); if (nextUserId) { const schedulingId = live.schedulingIdentifier || "➡️"; const handoffMsg = `<@${nextUserId}>${schedulingId}`; await sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger); } else { api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`); } } } } catch (err) { api.logger.warn(`dirigent: message_sent hook failed: ${String(err)}`); } }); }