fix(gateway): suppress heartbeat tool events in webchat (#80378)

This commit is contained in:
brokemac79
2026-05-12 18:45:16 +01:00
committed by GitHub
parent 45cf560f4e
commit 58700db5ff
2 changed files with 56 additions and 6 deletions
@@ -1067,6 +1067,44 @@ describe("agent event handler", () => {
resetAgentRunContextForTest();
});
it("suppresses heartbeat tool events for Control UI and verbose node subscribers", () => {
const {
broadcastToConnIds,
nodeSendToSession,
sessionEventSubscribers,
toolEventRecipients,
handler,
} = createHarness({
resolveSessionKeyForRun: () => "session-heartbeat",
});
registerAgentRunContext("run-heartbeat-tool", {
sessionKey: "session-heartbeat",
isHeartbeat: true,
verboseLevel: "on",
});
toolEventRecipients.add("run-heartbeat-tool", "conn-run");
sessionEventSubscribers.subscribe("conn-session");
handler({
runId: "run-heartbeat-tool",
seq: 1,
stream: "tool",
ts: 1_234,
data: {
phase: "start",
name: "read",
toolCallId: "tool-heartbeat-1",
args: { path: "HEARTBEAT.md" },
},
});
expect(broadcastToConnIds).not.toHaveBeenCalled();
const nodeToolCalls = nodeSendToSession.mock.calls.filter(([, event]) => event === "agent");
expect(nodeToolCalls).toHaveLength(0);
resetAgentRunContextForTest();
});
it("hydrates run-scoped tool events with session ownership metadata", () => {
const { broadcastToConnIds, toolEventRecipients, handler } = createHarness({
resolveSessionKeyForRun: () => "session-1",
+18 -6
View File
@@ -120,6 +120,10 @@ function shouldHideHeartbeatChatOutput(runId: string, sourceRunId?: string): boo
}
}
function shouldSuppressHeartbeatToolEvents(runId: string, sourceRunId?: string): boolean {
return Boolean(resolveHeartbeatContext(runId, sourceRunId)?.isHeartbeat);
}
function normalizeHeartbeatChatFinalText(params: {
runId: string;
sourceRunId?: string;
@@ -659,6 +663,8 @@ export function createAgentEventHandler({
const isToolEvent = evt.stream === "tool";
const isItemEvent = evt.stream === "item";
const toolVerbose = isToolEvent ? resolveToolVerboseLevel(evt.runId, sessionKey) : "off";
const suppressHeartbeatToolEvents =
isToolEvent && shouldSuppressHeartbeatToolEvents(clientRunId, evt.runId);
// Channel/node subscribers respect verbose; authenticated Control UI
// recipients need tool result payloads to render live tool cards.
const channelToolPayload =
@@ -690,7 +696,13 @@ export function createAgentEventHandler({
const toolPhase = typeof evt.data?.phase === "string" ? evt.data.phase : "";
// Flush pending assistant text before tool-start events so clients can
// render complete pre-tool text above tool cards (not truncated by delta throttle).
if (toolPhase === "start" && isControlUiVisible && sessionKey && !isAborted) {
if (
toolPhase === "start" &&
isControlUiVisible &&
sessionKey &&
!isAborted &&
!suppressHeartbeatToolEvents
) {
flushBufferedChatDeltaIfNeeded(sessionKey, clientRunId, evt.runId, evt.seq);
}
// Always broadcast tool events to registered WS recipients with
@@ -698,7 +710,7 @@ export function createAgentEventHandler({
// setting only controls whether tool details are sent as channel
// messages to messaging surfaces (Telegram, Discord, etc.).
const recipients = toolEventRecipients.get(evt.runId);
if (isControlUiVisible && recipients && recipients.size > 0) {
if (isControlUiVisible && !suppressHeartbeatToolEvents && recipients && recipients.size > 0) {
broadcastToConnIds(
"agent",
sessionKey ? { ...agentPayload, ...buildSessionEventSnapshot(sessionKey) } : agentPayload,
@@ -710,7 +722,7 @@ export function createAgentEventHandler({
// not know the runId in advance, so they cannot register as run-scoped
// tool recipients. Mirror tool lifecycle onto a session-scoped event so
// they can render live pending tool cards without polling history.
if (isControlUiVisible && sessionKey) {
if (isControlUiVisible && sessionKey && !suppressHeartbeatToolEvents) {
const sessionSubscribers = sessionEventSubscribers.getAll();
if (sessionSubscribers.size > 0) {
broadcastToConnIds(
@@ -732,9 +744,9 @@ export function createAgentEventHandler({
}
if (isControlUiVisible && sessionKey) {
// Send tool events to node/channel subscribers only when verbose is enabled;
// WS clients already received the event above via broadcastToConnIds.
if (!isToolEvent || toolVerbose !== "off") {
// Send non-heartbeat tool events to node/channel subscribers only when
// verbose is enabled; WS clients already received the event above.
if (!isToolEvent || (!suppressHeartbeatToolEvents && toolVerbose !== "off")) {
nodeSendToSession(
sessionKey,
"agent",