diff --git a/src/gateway/server-chat.agent-events.test.ts b/src/gateway/server-chat.agent-events.test.ts index e02ed25eb42..b5aed70e8c9 100644 --- a/src/gateway/server-chat.agent-events.test.ts +++ b/src/gateway/server-chat.agent-events.test.ts @@ -548,7 +548,16 @@ describe("agent event handler", () => { emitLifecycleEnd(handler, "run-heartbeat-alert"); - const payload = expectSingleFinalChatPayload(broadcast) as { + // The flush-before-final path may emit a delta with the pending tail + // followed by the authoritative final. Assert on content, not count. + const chatCalls = chatBroadcastCalls(broadcast); + expect(chatCalls.length).toBeGreaterThanOrEqual(1); + + const finalCall = chatCalls.find( + (c) => (c[1] as { state?: string }).state === "final", + ); + expect(finalCall).toBeDefined(); + const payload = finalCall![1] as { message?: { content?: Array<{ text?: string }> }; }; expect(payload.message?.content?.[0]?.text).toBe( diff --git a/src/gateway/server-chat.ts b/src/gateway/server-chat.ts index 36ce78903b8..f6aeeb2aedd 100644 --- a/src/gateway/server-chat.ts +++ b/src/gateway/server-chat.ts @@ -358,13 +358,24 @@ export function createAgentEventHandler({ const text = normalizedHeartbeatText.text.trim(); const shouldSuppressSilent = normalizedHeartbeatText.suppress || isSilentReplyText(text, SILENT_REPLY_TOKEN); + const shouldSuppressSilentLeadFragment = isSilentReplyLeadFragment(text); + const shouldSuppressHeartbeatStreaming = shouldHideHeartbeatChatOutput( + clientRunId, + sourceRunId, + ); // Flush any throttled delta so streaming clients receive the complete text // before the final event. The 150 ms throttle in emitChatDelta may have // suppressed the most recent chunk, leaving the client with stale text. // Only flush if the buffer has grown since the last broadcast to avoid duplicates. - if (text && !shouldSuppressSilent) { + if ( + text && + !shouldSuppressSilent && + !shouldSuppressSilentLeadFragment && + !shouldSuppressHeartbeatStreaming + ) { const lastBroadcastLen = chatRunState.deltaLastBroadcastLen.get(clientRunId) ?? 0; if (text.length > lastBroadcastLen) { + const pendingTail = text.slice(lastBroadcastLen); const flushPayload = { runId: clientRunId, sessionKey, @@ -372,12 +383,13 @@ export function createAgentEventHandler({ state: "delta" as const, message: { role: "assistant", - content: [{ type: "text", text }], + content: [{ type: "text", text: pendingTail }], timestamp: Date.now(), }, }; broadcast("chat", flushPayload, { dropIfSlow: true }); nodeSendToSession(sessionKey, "chat", flushPayload); + chatRunState.deltaLastBroadcastLen.set(clientRunId, text.length); } } chatRunState.deltaLastBroadcastLen.delete(clientRunId);