fix: advance turn in before_message_write to prevent race condition
When a speaker finishes with an end symbol, the turn was only advanced in the message_sent hook. But by that time, the message had already been broadcast to other agents, whose before_model_resolve ran with the old turn state, causing them to be blocked by the turn gate (forced no-reply). Fix: - Move turn advance for both NO_REPLY and end-symbol cases to before_message_write, which fires before the message is broadcast. - Guard 1: Only the current speaker can advance the turn (accountId check). - Guard 2: Only process assistant messages (role check). before_message_write fires for incoming user messages too, which contain end symbols from other agents and would cause cascading turn advances. - Use sessionTurnHandled set to prevent double-advancing in message_sent.
This commit is contained in:
16
dist/whispergate/index.ts
vendored
16
dist/whispergate/index.ts
vendored
@@ -932,9 +932,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract content from event.message (AgentMessage)
|
// Extract content from event.message (AgentMessage)
|
||||||
|
// Only process assistant messages — before_message_write fires for both
|
||||||
|
// user (incoming) and assistant (outgoing) messages. Incoming messages may
|
||||||
|
// contain end symbols from OTHER agents, which would incorrectly advance the turn.
|
||||||
let content = "";
|
let content = "";
|
||||||
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
const role = msg.role as string | undefined;
|
||||||
|
if (role && role !== "assistant") return;
|
||||||
// AgentMessage may have content as string or nested
|
// AgentMessage may have content as string or nested
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === "string") {
|
||||||
content = msg.content;
|
content = msg.content;
|
||||||
@@ -960,6 +965,17 @@ export default {
|
|||||||
|
|
||||||
if (!key || !channelId || !accountId) return;
|
if (!key || !channelId || !accountId) return;
|
||||||
|
|
||||||
|
// Only the current speaker should advance the turn.
|
||||||
|
// Other agents also trigger before_message_write (for incoming messages or forced no-reply),
|
||||||
|
// but they must not affect turn state.
|
||||||
|
const currentTurn = getTurnDebugInfo(channelId);
|
||||||
|
if (currentTurn.currentSpeaker !== accountId) {
|
||||||
|
api.logger.info(
|
||||||
|
`whispergate: before_message_write skipping non-current-speaker session=${key} accountId=${accountId} currentSpeaker=${currentTurn.currentSpeaker}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
||||||
ensurePolicyStateLoaded(api, live);
|
ensurePolicyStateLoaded(api, live);
|
||||||
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
||||||
|
|||||||
@@ -932,9 +932,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract content from event.message (AgentMessage)
|
// Extract content from event.message (AgentMessage)
|
||||||
|
// Only process assistant messages — before_message_write fires for both
|
||||||
|
// user (incoming) and assistant (outgoing) messages. Incoming messages may
|
||||||
|
// contain end symbols from OTHER agents, which would incorrectly advance the turn.
|
||||||
let content = "";
|
let content = "";
|
||||||
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
const role = msg.role as string | undefined;
|
||||||
|
if (role && role !== "assistant") return;
|
||||||
// AgentMessage may have content as string or nested
|
// AgentMessage may have content as string or nested
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === "string") {
|
||||||
content = msg.content;
|
content = msg.content;
|
||||||
@@ -960,6 +965,17 @@ export default {
|
|||||||
|
|
||||||
if (!key || !channelId || !accountId) return;
|
if (!key || !channelId || !accountId) return;
|
||||||
|
|
||||||
|
// Only the current speaker should advance the turn.
|
||||||
|
// Other agents also trigger before_message_write (for incoming messages or forced no-reply),
|
||||||
|
// but they must not affect turn state.
|
||||||
|
const currentTurn = getTurnDebugInfo(channelId);
|
||||||
|
if (currentTurn.currentSpeaker !== accountId) {
|
||||||
|
api.logger.info(
|
||||||
|
`whispergate: before_message_write skipping non-current-speaker session=${key} accountId=${accountId} currentSpeaker=${currentTurn.currentSpeaker}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
||||||
ensurePolicyStateLoaded(api, live);
|
ensurePolicyStateLoaded(api, live);
|
||||||
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
||||||
|
|||||||
Reference in New Issue
Block a user