mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(gateway): suppress heartbeat tool events in webchat (#80378)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user