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
This commit is contained in:
zhi
2026-03-01 09:13:03 +00:00
parent 9e754d32cf
commit 435a7712b8

View File

@@ -26,6 +26,7 @@ type DebugConfig = {
const sessionDecision = new Map<string, DecisionRecord>();
const sessionAllowed = new Map<string, boolean>(); // Track if session was allowed to speak (true) or forced no-reply (false)
const sessionInjected = new Set<string>(); // Track which sessions have already injected the end marker
const sessionChannelId = new Map<string, string>(); // 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<string, unknown>)?.deliveryContext as Record<string, unknown> | 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<string, unknown>).message) {
const msg = (event as Record<string, unknown>).message as Record<string, unknown>;
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`,
);
}
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"}`,
);
}
// 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;