fix: channelId extraction, sender identification, and per-channel turn order #9

Merged
hzhang merged 18 commits from fix/turn-gate-and-handoff into main 2026-03-01 11:12:18 +00:00
Showing only changes of commit 435a7712b8 - Show all commits

View File

@@ -26,6 +26,7 @@ type DebugConfig = {
const sessionDecision = new Map<string, DecisionRecord>(); 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 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 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 MAX_SESSION_DECISIONS = 2000;
const DECISION_TTL_MS = 5 * 60 * 1000; const DECISION_TTL_MS = 5 * 60 * 1000;
function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean): string { function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean): string {
@@ -636,6 +637,10 @@ export default {
} }
// Allowed to speak - record this session as allowed // Allowed to speak - record this session as allowed
sessionAllowed.set(key, true); 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 // NOTE: This hook is synchronous, do not use async/await
api.on("before_message_write", (event, ctx) => { api.on("before_message_write", (event, ctx) => {
try { try {
const key = ctx.sessionKey; // Debug: print all available keys in event and ctx
const channelId = ctx.channelId as string | undefined; api.logger.info(
const accountId = ctx.accountId as string | undefined; `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; if (!key || !channelId || !accountId) return;
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig; 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 trimmed = content.trim();
const isNoReply = /^NO_REPLY$/i.test(trimmed); 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 // Check if this session was forced no-reply or allowed to speak
const wasAllowed = sessionAllowed.get(key); 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 === undefined) return; // No record, skip
if (wasAllowed === false) { if (wasAllowed === false) {
// Forced no-reply - do not advance turn // Forced no-reply - do not advance turn
sessionAllowed.delete(key); sessionAllowed.delete(key);
if (shouldDebugLog(live, channelId)) { api.logger.info(
api.logger.info( `whispergate: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`,
`whispergate: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`, );
);
}
return; return;
} }
@@ -844,11 +890,9 @@ export default {
sessionAllowed.delete(key); sessionAllowed.delete(key);
if (shouldDebugLog(live, channelId)) { api.logger.info(
api.logger.info( `whispergate: before_message_write real no-reply session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
`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 all agents NO_REPLY'd (dormant), don't trigger handoff
if (!nextSpeaker) { if (!nextSpeaker) {
@@ -880,9 +924,19 @@ export default {
// Turn advance: when an agent sends a message, check if it signals end of turn // Turn advance: when an agent sends a message, check if it signals end of turn
api.on("message_sent", async (event, ctx) => { api.on("message_sent", async (event, ctx) => {
try { try {
const channelId = ctx.channelId; const key = ctx.sessionKey;
const accountId = ctx.accountId; // Try ctx.channelId first, fallback to sessionChannelId mapping
const content = event.content || ""; 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; if (!channelId || !accountId) return;