diff --git a/no-reply-api/server.mjs b/no-reply-api/server.mjs index c04927a..da9dd70 100644 --- a/no-reply-api/server.mjs +++ b/no-reply-api/server.mjs @@ -1,7 +1,7 @@ import http from "node:http"; const port = Number(process.env.PORT || 8787); -const modelName = process.env.NO_REPLY_MODEL || "dirigent-no-reply-v1"; +const modelName = process.env.NO_REPLY_MODEL || "no-reply"; const authToken = process.env.AUTH_TOKEN || ""; function sendJson(res, status, payload) { diff --git a/plugin/hooks/before-message-write.ts b/plugin/hooks/before-message-write.ts index faee8ba..b2422c0 100644 --- a/plugin/hooks/before-message-write.ts +++ b/plugin/hooks/before-message-write.ts @@ -62,9 +62,25 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo let content = ""; const msg = (event as Record).message as Record | undefined; + const msgContent = msg?.content; if (msg) { const role = msg.role as string | undefined; if (role && role !== "assistant") return; + + // Detect tool calls — intermediate model step, not a final response. + // Skip turn processing entirely to avoid false NO_REPLY detection. + if (Array.isArray(msgContent)) { + const hasToolCalls = (msgContent as Record[]).some( + (part) => part?.type === "toolCall" || part?.type === "tool_call" || part?.type === "tool_use", + ); + if (hasToolCalls) { + api.logger.info( + `dirigent: before_message_write skipping tool-call message session=${key ?? "undefined"} channel=${channelId ?? "undefined"}`, + ); + return; + } + } + if (typeof msg.content === "string") { content = msg.content; } else if (Array.isArray(msg.content)) { @@ -99,13 +115,15 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record); const trimmed = content.trim(); - const isEmpty = trimmed.length === 0; const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed); const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : ""; const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar); const waitId = live.waitIdentifier || "👤"; const hasWaitIdentifier = !!lastChar && lastChar === waitId; - const wasNoReply = isEmpty || isNoReply; + // Only treat explicit NO_REPLY/HEARTBEAT_OK keywords 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 turnDebug = getTurnDebugInfo(channelId); api.logger.info( diff --git a/plugin/hooks/message-sent.ts b/plugin/hooks/message-sent.ts index 2c87284..20b2f7e 100644 --- a/plugin/hooks/message-sent.ts +++ b/plugin/hooks/message-sent.ts @@ -74,13 +74,13 @@ export function registerMessageSentHook(deps: MessageSentDeps): void { const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record); const trimmed = content.trim(); - const isEmpty = trimmed.length === 0; const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed); const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : ""; const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar); const waitId = live.waitIdentifier || "👤"; const hasWaitIdentifier = !!lastChar && lastChar === waitId; - const wasNoReply = isEmpty || isNoReply; + // Only treat explicit NO_REPLY/HEARTBEAT_OK keywords as no-reply. + const wasNoReply = isNoReply; if (key && sessionTurnHandled.has(key)) { sessionTurnHandled.delete(key); @@ -100,7 +100,7 @@ export function registerMessageSentHook(deps: MessageSentDeps): void { if (wasNoReply || hasEndSymbol) { const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply); - const trigger = wasNoReply ? (isEmpty ? "empty" : "no_reply_keyword") : "end_symbol"; + const trigger = wasNoReply ? "no_reply_keyword" : "end_symbol"; api.logger.info( `dirigent: turn onSpeakerDone channel=${channelId} from=${accountId} next=${nextSpeaker ?? "dormant"} trigger=${trigger}`, );