fix: concluded discussions suppress turns and send single auto-reply
Two bugs in concluded discussion channel handling: 1. before_model_resolve did not check rec.discussion.concluded, so it still initialized the speaker list and ran turn management. Fixed by returning NO_REPLY early for concluded discussions (same as report mode). 2. message_received fired for all agent VM contexts, causing multiple "This discussion is closed" auto-replies per incoming message. Fixed with process-level dedup keyed on channelId:messageId (same pattern as agent_end runId dedup). Also fixed message_id extraction to look in metadata.conversation_info.message_id first. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,12 @@ export function registerBeforeModelResolveHook(deps: Deps): void {
|
|||||||
// dead mode: suppress all responses
|
// dead mode: suppress all responses
|
||||||
if (mode === "report" || mode === "dead" as string) return NO_REPLY;
|
if (mode === "report" || mode === "dead" as string) return NO_REPLY;
|
||||||
|
|
||||||
|
// concluded discussion: suppress all agent responses (auto-reply handled by message_received)
|
||||||
|
if (mode === "discussion") {
|
||||||
|
const rec = channelStore.getRecord(channelId);
|
||||||
|
if (rec.discussion?.concluded) return NO_REPLY;
|
||||||
|
}
|
||||||
|
|
||||||
// disabled modes: let agents respond freely
|
// disabled modes: let agents respond freely
|
||||||
if (mode === "none" || mode === "work") return;
|
if (mode === "none" || mode === "work") return;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ type Deps = {
|
|||||||
interruptTailMatch: InterruptFn;
|
interruptTailMatch: InterruptFn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process-level dedup for concluded-discussion auto-replies.
|
||||||
|
* Multiple agent VM contexts all fire message_received for the same incoming message;
|
||||||
|
* only the first should send the "This discussion is closed" reply.
|
||||||
|
* Keyed on channelId:messageId; evicted after 500 entries.
|
||||||
|
*/
|
||||||
|
const _CONCLUDED_REPLY_DEDUP_KEY = "_dirigentConcludedReplyDedup";
|
||||||
|
if (!(globalThis as Record<string, unknown>)[_CONCLUDED_REPLY_DEDUP_KEY]) {
|
||||||
|
(globalThis as Record<string, unknown>)[_CONCLUDED_REPLY_DEDUP_KEY] = new Set<string>();
|
||||||
|
}
|
||||||
|
const concludedReplyDedup: Set<string> = (globalThis as Record<string, unknown>)[_CONCLUDED_REPLY_DEDUP_KEY] as Set<string>;
|
||||||
|
|
||||||
export function registerMessageReceivedHook(deps: Deps): void {
|
export function registerMessageReceivedHook(deps: Deps): void {
|
||||||
const { api, channelStore, identityRegistry, moderatorBotToken, scheduleIdentifier, interruptTailMatch } = deps;
|
const { api, channelStore, identityRegistry, moderatorBotToken, scheduleIdentifier, interruptTailMatch } = deps;
|
||||||
// Derive the moderator bot's own Discord user ID so we can skip self-messages
|
// Derive the moderator bot's own Discord user ID so we can skip self-messages
|
||||||
@@ -61,15 +73,31 @@ export function registerMessageReceivedHook(deps: Deps): void {
|
|||||||
// but we handle archived auto-reply here)
|
// but we handle archived auto-reply here)
|
||||||
if (mode === "report") return;
|
if (mode === "report") return;
|
||||||
|
|
||||||
// archived: auto-reply via moderator
|
// archived: auto-reply via moderator (deduped — only one agent instance should reply)
|
||||||
if (mode === "discussion") {
|
if (mode === "discussion") {
|
||||||
const rec = channelStore.getRecord(channelId);
|
const rec = channelStore.getRecord(channelId);
|
||||||
if (rec.discussion?.concluded && moderatorBotToken) {
|
if (rec.discussion?.concluded && moderatorBotToken) {
|
||||||
await sendModeratorMessage(
|
const metadata = e.metadata as Record<string, unknown> | undefined;
|
||||||
moderatorBotToken, channelId,
|
const convInfo = metadata?.conversation_info as Record<string, unknown> | undefined;
|
||||||
"This discussion is closed and no longer active.",
|
const incomingMsgId = String(
|
||||||
api.logger,
|
convInfo?.message_id ??
|
||||||
).catch(() => undefined);
|
metadata?.message_id ??
|
||||||
|
metadata?.messageId ??
|
||||||
|
e.id ?? "",
|
||||||
|
);
|
||||||
|
const dedupKey = `${channelId}:${incomingMsgId}`;
|
||||||
|
if (!concludedReplyDedup.has(dedupKey)) {
|
||||||
|
concludedReplyDedup.add(dedupKey);
|
||||||
|
if (concludedReplyDedup.size > 500) {
|
||||||
|
const oldest = concludedReplyDedup.values().next().value;
|
||||||
|
if (oldest) concludedReplyDedup.delete(oldest);
|
||||||
|
}
|
||||||
|
await sendModeratorMessage(
|
||||||
|
moderatorBotToken, channelId,
|
||||||
|
"This discussion is closed and no longer active.",
|
||||||
|
api.logger,
|
||||||
|
).catch(() => undefined);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user