fix/gateway-keywords-no-empty #19

Merged
hzhang merged 2 commits from fix/gateway-keywords-no-empty into main 2026-03-10 13:56:45 +00:00
3 changed files with 13 additions and 7 deletions
Showing only changes of commit e6b20e9d52 - Show all commits

View File

@@ -115,14 +115,16 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>); const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>);
const trimmed = content.trim(); const trimmed = content.trim();
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed); const isNoReply =
trimmed.length === 0 ||
/^NO$/i.test(trimmed) ||
/^NO_REPLY$/i.test(trimmed) ||
/^HEARTBEAT_OK$/i.test(trimmed);
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : ""; const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar); const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
const waitId = live.waitIdentifier || "👤"; const waitId = live.waitIdentifier || "👤";
const hasWaitIdentifier = !!lastChar && lastChar === waitId; const hasWaitIdentifier = !!lastChar && lastChar === waitId;
// Only treat explicit NO_REPLY/HEARTBEAT_OK keywords as no-reply. // Treat explicit NO_REPLY/HEARTBEAT_OK/NO and empty responses as no-reply.
// Empty content is NOT treated as no-reply — it may come from intermediate
// model responses (e.g. thinking-only blocks) that are not final answers.
const wasNoReply = isNoReply; const wasNoReply = isNoReply;
const turnDebug = getTurnDebugInfo(channelId); const turnDebug = getTurnDebugInfo(channelId);

View File

@@ -74,12 +74,16 @@ export function registerMessageSentHook(deps: MessageSentDeps): void {
const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>); const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>);
const trimmed = content.trim(); const trimmed = content.trim();
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed); const isNoReply =
trimmed.length === 0 ||
/^NO$/i.test(trimmed) ||
/^NO_REPLY$/i.test(trimmed) ||
/^HEARTBEAT_OK$/i.test(trimmed);
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : ""; const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar); const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
const waitId = live.waitIdentifier || "👤"; const waitId = live.waitIdentifier || "👤";
const hasWaitIdentifier = !!lastChar && lastChar === waitId; const hasWaitIdentifier = !!lastChar && lastChar === waitId;
// Only treat explicit NO_REPLY/HEARTBEAT_OK keywords as no-reply. // Treat explicit NO_REPLY/HEARTBEAT_OK/NO and empty responses as no-reply.
const wasNoReply = isNoReply; const wasNoReply = isNoReply;
if (key && sessionTurnHandled.has(key)) { if (key && sessionTurnHandled.has(key)) {

View File

@@ -36,7 +36,7 @@ type DebugConfig = {
function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean, schedulingIdentifier: string, waitIdentifier: string): string { function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean, schedulingIdentifier: string, waitIdentifier: string): string {
const symbols = endSymbols.length > 0 ? endSymbols.join("") : "🔚"; const symbols = endSymbols.length > 0 ? endSymbols.join("") : "🔚";
let instruction = `Your response MUST end with ${symbols}. Exception: gateway keywords (e.g. NO_REPLY, HEARTBEAT_OK) must NOT include ${symbols}.`; let instruction = `Your response MUST end with ${symbols}. Exception: gateway keywords (e.g. NO_REPLY, HEARTBEAT_OK, NO, or an empty response) must NOT include ${symbols}.`;
if (isGroupChat) { if (isGroupChat) {
instruction += `\n\nGroup chat rules: If this message is not relevant to you, does not need your response, or you have nothing valuable to add, reply with NO_REPLY. Do not speak just for the sake of speaking.`; instruction += `\n\nGroup chat rules: If this message is not relevant to you, does not need your response, or you have nothing valuable to add, reply with NO_REPLY. Do not speak just for the sake of speaking.`;
instruction += `\n\nWait for human reply: If you need a human to respond to your message, end with ${waitIdentifier} instead of ${symbols}. This pauses all agents until a human speaks. Use this sparingly — only when you are confident the human is actively participating in the discussion (has sent a message recently). Do NOT use it speculatively.`; instruction += `\n\nWait for human reply: If you need a human to respond to your message, end with ${waitIdentifier} instead of ${symbols}. This pauses all agents until a human speaks. Use this sparingly — only when you are confident the human is actively participating in the discussion (has sent a message recently). Do NOT use it speculatively.`;