From 435a7712b8135c17fae94e39797a762947625930 Mon Sep 17 00:00:00 2001 From: zhi Date: Sun, 1 Mar 2026 09:13:03 +0000 Subject: [PATCH] fix: add sessionChannelId mapping for message_sent - Add sessionChannelId Map to track sessionKey -> channelId - Save channelId in before_model_resolve when we have derived.channelId - Fix message_sent to use sessionChannelId fallback when ctx.channelId is undefined - Add debug logging to message_sent --- plugin/index.ts | 92 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/plugin/index.ts b/plugin/index.ts index ddbb311..3fcbc8b 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -26,6 +26,7 @@ type DebugConfig = { const sessionDecision = new Map(); const sessionAllowed = new Map(); // Track if session was allowed to speak (true) or forced no-reply (false) const sessionInjected = new Set(); // Track which sessions have already injected the end marker +const sessionChannelId = new Map(); // Track sessionKey -> channelId mapping const MAX_SESSION_DECISIONS = 2000; const DECISION_TTL_MS = 5 * 60 * 1000; function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean): string { @@ -636,6 +637,10 @@ export default { } // Allowed to speak - record this session as allowed sessionAllowed.set(key, true); + // Also save channelId for this session + if (derived.channelId) { + sessionChannelId.set(key, derived.channelId); + } } } @@ -808,33 +813,74 @@ export default { // NOTE: This hook is synchronous, do not use async/await api.on("before_message_write", (event, ctx) => { try { - const key = ctx.sessionKey; - const channelId = ctx.channelId as string | undefined; - const accountId = ctx.accountId as string | undefined; + // Debug: print all available keys in event and ctx + api.logger.info( + `whispergate: DEBUG before_message_write eventKeys=${JSON.stringify(Object.keys(event ?? {}))} ctxKeys=${JSON.stringify(Object.keys(ctx ?? {}))}`, + ); + + // Try multiple sources for channelId and accountId + const deliveryContext = (ctx as Record)?.deliveryContext as Record | undefined; + let key = ctx.sessionKey; + let channelId = ctx.channelId as string | undefined; + let accountId = ctx.accountId as string | undefined; + let content = (event.content as string) || ""; + + // Fallback: get channelId from deliveryContext.to + if (!channelId && deliveryContext?.to) { + const toStr = String(deliveryContext.to); + channelId = toStr.startsWith("channel:") ? toStr.replace("channel:", "") : toStr; + } + + // Fallback: get accountId from deliveryContext.accountId + if (!accountId && deliveryContext?.accountId) { + accountId = String(deliveryContext.accountId); + } + + // Fallback: get content from event.message.content + if (!content && (event as Record).message) { + const msg = (event as Record).message as Record; + content = String(msg.content ?? ""); + } + + // Always log for debugging - show all available info + api.logger.info( + `whispergate: DEBUG before_message_write session=${key ?? "undefined"} channel=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} contentType=${typeof content} content=${String(content).slice(0, 200)}`, + ); if (!key || !channelId || !accountId) return; const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig; - // Get the agent's output content - const content = (event.content as string) || ""; const trimmed = content.trim(); const isNoReply = /^NO_REPLY$/i.test(trimmed); - if (!isNoReply) return; + // Log turn state for debugging + const turnDebug = getTurnDebugInfo(channelId); + api.logger.info( + `whispergate: DEBUG turn state channel=${channelId} turnOrder=${JSON.stringify(turnDebug.turnOrder)} currentSpeaker=${turnDebug.currentSpeaker} noRepliedThisCycle=${JSON.stringify([...turnDebug.noRepliedThisCycle])}`, + ); + + if (!isNoReply) { + api.logger.info( + `whispergate: before_message_write content is not NO_REPLY, skipping channel=${channelId}`, + ); + return; + } // Check if this session was forced no-reply or allowed to speak const wasAllowed = sessionAllowed.get(key); + api.logger.info( + `whispergate: DEBUG NO_REPLY detected session=${key} wasAllowed=${wasAllowed}`, + ); + if (wasAllowed === undefined) return; // No record, skip if (wasAllowed === false) { // Forced no-reply - do not advance turn sessionAllowed.delete(key); - if (shouldDebugLog(live, channelId)) { - api.logger.info( - `whispergate: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`, - ); - } + api.logger.info( + `whispergate: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`, + ); return; } @@ -844,11 +890,9 @@ export default { sessionAllowed.delete(key); - if (shouldDebugLog(live, channelId)) { - api.logger.info( - `whispergate: before_message_write real no-reply session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`, - ); - } + api.logger.info( + `whispergate: before_message_write real no-reply session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`, + ); // If all agents NO_REPLY'd (dormant), don't trigger handoff if (!nextSpeaker) { @@ -880,9 +924,19 @@ export default { // Turn advance: when an agent sends a message, check if it signals end of turn api.on("message_sent", async (event, ctx) => { try { - const channelId = ctx.channelId; - const accountId = ctx.accountId; - const content = event.content || ""; + const key = ctx.sessionKey; + // Try ctx.channelId first, fallback to sessionChannelId mapping + let channelId = ctx.channelId as string | undefined; + if (!channelId && key) { + channelId = sessionChannelId.get(key); + } + const accountId = ctx.accountId as string | undefined; + const content = (event.content as string) || ""; + + // Debug log + api.logger.info( + `whispergate: DEBUG message_sent session=${key ?? "undefined"} channelId=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} content=${content.slice(0, 100)}`, + ); if (!channelId || !accountId) return;