diff --git a/src/agents/embedded-agent-subscribe.handlers.messages.test-support.ts b/src/agents/embedded-agent-subscribe.handlers.messages.test-support.ts index d6c104806f57..f7850b6d8bb7 100644 --- a/src/agents/embedded-agent-subscribe.handlers.messages.test-support.ts +++ b/src/agents/embedded-agent-subscribe.handlers.messages.test-support.ts @@ -39,10 +39,6 @@ type EmbeddedSubscribeMessagesTestApi = { visibleDelta: string; }, ): { hold: boolean; text: string }; - resolveSilentReplyFallbackText(params: { - text: unknown; - messagingToolSentTexts: string[]; - }): string; }; function getTestApi(): EmbeddedSubscribeMessagesTestApi { @@ -79,10 +75,3 @@ export function resolveCurrentSourceMessagingToolPartial( ): { hold: boolean; text: string } { return getTestApi().resolveCurrentSourceMessagingToolPartial(state, params); } - -export function resolveSilentReplyFallbackText(params: { - text: unknown; - messagingToolSentTexts: string[]; -}): string { - return getTestApi().resolveSilentReplyFallbackText(params); -} diff --git a/src/agents/embedded-agent-subscribe.handlers.messages.test.ts b/src/agents/embedded-agent-subscribe.handlers.messages.test.ts index bba3cf0ec884..25516f954a13 100644 --- a/src/agents/embedded-agent-subscribe.handlers.messages.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.messages.test.ts @@ -16,7 +16,6 @@ import { buildAssistantStreamData, recordPendingAssistantReplyDirectives, resolveCurrentSourceMessagingToolPartial, - resolveSilentReplyFallbackText, } from "./embedded-agent-subscribe.handlers.messages.test-support.js"; import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; import { @@ -237,50 +236,6 @@ function createMessageToolEnvelope(message: string, args: Record { - it("replaces NO_REPLY with latest messaging tool text when available", () => { - expect( - resolveSilentReplyFallbackText({ - text: "NO_REPLY", - messagingToolSentTexts: ["first", "final delivered text"], - }), - ).toBe("final delivered text"); - }); - - it("keeps original text when response is not NO_REPLY", () => { - expect( - resolveSilentReplyFallbackText({ - text: "normal assistant reply", - messagingToolSentTexts: ["final delivered text"], - }), - ).toBe("normal assistant reply"); - }); - - it("keeps NO_REPLY when there is no messaging tool text to mirror", () => { - expect( - resolveSilentReplyFallbackText({ - text: "NO_REPLY", - messagingToolSentTexts: [], - }), - ).toBe("NO_REPLY"); - }); - - it("tolerates malformed text payloads without throwing", () => { - expect( - resolveSilentReplyFallbackText({ - text: undefined, - messagingToolSentTexts: ["final delivered text"], - }), - ).toBe(""); - expect( - resolveSilentReplyFallbackText({ - text: "NO_REPLY", - messagingToolSentTexts: [42 as unknown as string], - }), - ).toBe("42"); - }); -}); - describe("hasAssistantVisibleReply", () => { it("treats audio-only payloads as visible", () => { expect(hasAssistantVisibleReply({ audioAsVoice: true })).toBe(true); @@ -1705,6 +1660,71 @@ describe("handleMessageEnd", () => { }, ); + it("keeps exact NO_REPLY silent after a user-facing message send followed by sessions_send (#119383)", () => { + const emitBlockReply = vi.fn(); + const finalizeAssistantTexts = vi.fn(); + const ctx = createMessageEndContext({ + emitBlockReply, + finalizeAssistantTexts, + consumeReplyDirectives: vi.fn((text: string) => ({ text })), + state: { + blockBuffer: "", + deltaBuffer: "", + messagingToolSentTexts: ["", ""], + messagingToolSentTextsNormalized: ["", ""], + messagingToolSentTargets: [ + { + tool: "message", + provider: "whatsapp", + to: "user:123", + text: "", + }, + ], + }, + }); + + void endMessage(ctx, { + message: { role: "assistant", content: [{ type: "text", text: "NO_REPLY" }] }, + }); + + // The exact silent token must never be rewritten to the sessions_send body: + // the final assistant text keeps NO_REPLY and no block reply carries the note. + expect(finalizeAssistantTexts).toHaveBeenCalledWith( + expect.objectContaining({ text: "NO_REPLY" }), + ); + for (const call of emitBlockReply.mock.calls) { + expect(JSON.stringify(call)).not.toContain(""); + } + }); + + it("keeps exact NO_REPLY silent when only sessions_send delivered (#119383)", () => { + const emitBlockReply = vi.fn(); + const finalizeAssistantTexts = vi.fn(); + const ctx = createMessageEndContext({ + emitBlockReply, + finalizeAssistantTexts, + consumeReplyDirectives: vi.fn((text: string) => ({ text })), + state: { + blockBuffer: "", + deltaBuffer: "", + messagingToolSentTexts: [""], + messagingToolSentTextsNormalized: [""], + messagingToolSentTargets: [], + }, + }); + + void endMessage(ctx, { + message: { role: "assistant", content: [{ type: "text", text: "NO_REPLY" }] }, + }); + + expect(finalizeAssistantTexts).toHaveBeenCalledWith( + expect.objectContaining({ text: "NO_REPLY" }), + ); + for (const call of emitBlockReply.mock.calls) { + expect(JSON.stringify(call)).not.toContain(""); + } + }); + it.each([ { name: "counts a completed provider assistant message", diff --git a/src/agents/embedded-agent-subscribe.handlers.messages.ts b/src/agents/embedded-agent-subscribe.handlers.messages.ts index faa5fa61f77d..453853e407d5 100644 --- a/src/agents/embedded-agent-subscribe.handlers.messages.ts +++ b/src/agents/embedded-agent-subscribe.handlers.messages.ts @@ -446,23 +446,6 @@ function copyPartialBlockState( target.pendingTagFragment = source.pendingTagFragment; } -/** Replaces a silent-reply token with the latest sent messaging-tool text when available. */ -function resolveSilentReplyFallbackText(params: { - text: unknown; - messagingToolSentTexts: string[]; -}): string { - const text = coerceChatContentText(params.text); - const trimmed = text.trim(); - if (trimmed !== SILENT_REPLY_TOKEN) { - return text; - } - const fallback = coerceChatContentText(params.messagingToolSentTexts.at(-1)).trim(); - if (!fallback) { - return text; - } - return fallback; -} - function clearPendingToolMedia( state: Pick< EmbeddedAgentSubscribeState, @@ -1257,7 +1240,6 @@ if (process.env.VITEST || process.env.NODE_ENV === "test") { buildAssistantStreamData, recordPendingAssistantReplyDirectives, resolveCurrentSourceMessagingToolPartial, - resolveSilentReplyFallbackText, }; } @@ -1357,10 +1339,10 @@ export function handleMessageEnd( ? ctx.stripBlockTags(visibleText, { thinking: false, final: false }, { final: true }) : visibleText; - const text = resolveSilentReplyFallbackText({ - text: finalVisibleText, - messagingToolSentTexts: ctx.state.messagingToolSentTexts, - }); + // Exact NO_REPLY stays silent. The legacy rewrite (silentReplyRewrite) was + // removed by contract; global messaging-tool send evidence is not a + // user-route reply and must never be mirrored into the final payload. + const text = finalVisibleText; const rawThinking = ctx.state.includeReasoning || ctx.state.streamReasoning ? extractAssistantThinking(assistantMessage) || extractThinkingFromTaggedText(rawText)