Gateway/TUI: filter heartbeat ACK noise in chat events

This commit is contained in:
Vignesh Natarajan
2026-02-20 20:23:17 -08:00
parent 1ded4c672a
commit 2227840989
3 changed files with 196 additions and 17 deletions

View File

@@ -1,3 +1,4 @@
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "../auto-reply/heartbeat.js";
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { loadConfig } from "../config/config.js";
@@ -6,12 +7,37 @@ import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
import { loadSessionEntry } from "./session-utils.js";
import { formatForLog } from "./ws-log.js";
function resolveHeartbeatAckMaxChars(): number {
try {
const cfg = loadConfig();
return Math.max(
0,
cfg.agents?.defaults?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
);
} catch {
return DEFAULT_HEARTBEAT_ACK_MAX_CHARS;
}
}
function resolveHeartbeatContext(runId: string, sourceRunId?: string) {
const primary = getAgentRunContext(runId);
if (primary?.isHeartbeat) {
return primary;
}
if (sourceRunId && sourceRunId !== runId) {
const source = getAgentRunContext(sourceRunId);
if (source?.isHeartbeat) {
return source;
}
}
return primary;
}
/**
* Check if webchat broadcasts should be suppressed for heartbeat runs.
* Returns true if the run is a heartbeat and showOk is false.
* Check if heartbeat ACK/noise should be hidden from interactive chat surfaces.
*/
function shouldSuppressHeartbeatBroadcast(runId: string): boolean {
const runContext = getAgentRunContext(runId);
function shouldHideHeartbeatChatOutput(runId: string, sourceRunId?: string): boolean {
const runContext = resolveHeartbeatContext(runId, sourceRunId);
if (!runContext?.isHeartbeat) {
return false;
}
@@ -26,6 +52,28 @@ function shouldSuppressHeartbeatBroadcast(runId: string): boolean {
}
}
function normalizeHeartbeatChatFinalText(params: {
runId: string;
sourceRunId?: string;
text: string;
}): { suppress: boolean; text: string } {
if (!shouldHideHeartbeatChatOutput(params.runId, params.sourceRunId)) {
return { suppress: false, text: params.text };
}
const stripped = stripHeartbeatToken(params.text, {
mode: "heartbeat",
maxAckChars: resolveHeartbeatAckMaxChars(),
});
if (!stripped.didStrip) {
return { suppress: false, text: params.text };
}
if (stripped.shouldSkip) {
return { suppress: true, text: "" };
}
return { suppress: false, text: stripped.text };
}
export type ChatRunEntry = {
sessionKey: string;
clientRunId: string;
@@ -228,11 +276,20 @@ export function createAgentEventHandler({
clearAgentRunContext,
toolEventRecipients,
}: AgentEventHandlerOptions) {
const emitChatDelta = (sessionKey: string, clientRunId: string, seq: number, text: string) => {
const emitChatDelta = (
sessionKey: string,
clientRunId: string,
sourceRunId: string,
seq: number,
text: string,
) => {
if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
return;
}
chatRunState.buffers.set(clientRunId, text);
if (shouldHideHeartbeatChatOutput(clientRunId, sourceRunId)) {
return;
}
const now = Date.now();
const last = chatRunState.deltaSentAt.get(clientRunId) ?? 0;
if (now - last < 150) {
@@ -250,22 +307,27 @@ export function createAgentEventHandler({
timestamp: now,
},
};
// Suppress webchat broadcast for heartbeat runs when showOk is false
if (!shouldSuppressHeartbeatBroadcast(clientRunId)) {
broadcast("chat", payload, { dropIfSlow: true });
}
broadcast("chat", payload, { dropIfSlow: true });
nodeSendToSession(sessionKey, "chat", payload);
};
const emitChatFinal = (
sessionKey: string,
clientRunId: string,
sourceRunId: string,
seq: number,
jobState: "done" | "error",
error?: unknown,
) => {
const text = chatRunState.buffers.get(clientRunId)?.trim() ?? "";
const shouldSuppressSilent = isSilentReplyText(text, SILENT_REPLY_TOKEN);
const bufferedText = chatRunState.buffers.get(clientRunId)?.trim() ?? "";
const normalizedHeartbeatText = normalizeHeartbeatChatFinalText({
runId: clientRunId,
sourceRunId,
text: bufferedText,
});
const text = normalizedHeartbeatText.text.trim();
const shouldSuppressSilent =
normalizedHeartbeatText.suppress || isSilentReplyText(text, SILENT_REPLY_TOKEN);
chatRunState.buffers.delete(clientRunId);
chatRunState.deltaSentAt.delete(clientRunId);
if (jobState === "done") {
@@ -283,10 +345,7 @@ export function createAgentEventHandler({
}
: undefined,
};
// Suppress webchat broadcast for heartbeat runs when showOk is false
if (!shouldSuppressHeartbeatBroadcast(clientRunId)) {
broadcast("chat", payload);
}
broadcast("chat", payload);
nodeSendToSession(sessionKey, "chat", payload);
return;
}
@@ -388,7 +447,7 @@ export function createAgentEventHandler({
nodeSendToSession(sessionKey, "agent", isToolEvent ? toolPayload : agentPayload);
}
if (!isAborted && evt.stream === "assistant" && typeof evt.data?.text === "string") {
emitChatDelta(sessionKey, clientRunId, evt.seq, evt.data.text);
emitChatDelta(sessionKey, clientRunId, evt.runId, evt.seq, evt.data.text);
} else if (!isAborted && (lifecyclePhase === "end" || lifecyclePhase === "error")) {
if (chatLink) {
const finished = chatRunState.registry.shift(evt.runId);
@@ -399,6 +458,7 @@ export function createAgentEventHandler({
emitChatFinal(
finished.sessionKey,
finished.clientRunId,
evt.runId,
evt.seq,
lifecyclePhase === "error" ? "error" : "done",
evt.data?.error,
@@ -407,6 +467,7 @@ export function createAgentEventHandler({
emitChatFinal(
sessionKey,
eventRunId,
evt.runId,
evt.seq,
lifecyclePhase === "error" ? "error" : "done",
evt.data?.error,