feat: add heartbeat visibility filtering for webchat
- Add isHeartbeat to AgentRunContext to track heartbeat runs - Pass isHeartbeat flag through agent runner execution - Suppress webchat broadcast (deltas + final) for heartbeat runs when showOk is false - Webchat uses channels.defaults.heartbeat settings (no per-channel config) - Default behavior: hide HEARTBEAT_OK from webchat (matches other channels) This allows users to control whether heartbeat responses appear in the webchat UI via channels.defaults.heartbeat.showOk (defaults to false).
This commit is contained in:
@@ -89,6 +89,7 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
registerAgentRunContext(runId, {
|
registerAgentRunContext(runId, {
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
verboseLevel: params.resolvedVerboseLevel,
|
verboseLevel: params.resolvedVerboseLevel,
|
||||||
|
isHeartbeat: params.isHeartbeat,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
||||||
|
import { loadConfig } from "../config/config.js";
|
||||||
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
|
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
|
||||||
|
import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
|
||||||
import { loadSessionEntry } from "./session-utils.js";
|
import { loadSessionEntry } from "./session-utils.js";
|
||||||
import { formatForLog } from "./ws-log.js";
|
import { formatForLog } from "./ws-log.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if webchat broadcasts should be suppressed for heartbeat runs.
|
||||||
|
* Returns true if the run is a heartbeat and showOk is false.
|
||||||
|
*/
|
||||||
|
function shouldSuppressHeartbeatBroadcast(runId: string): boolean {
|
||||||
|
const runContext = getAgentRunContext(runId);
|
||||||
|
if (!runContext?.isHeartbeat) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const visibility = resolveHeartbeatVisibility({ cfg, channel: "webchat" });
|
||||||
|
return !visibility.showOk;
|
||||||
|
} catch {
|
||||||
|
// Default to suppressing if we can't load config
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type ChatRunEntry = {
|
export type ChatRunEntry = {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
clientRunId: string;
|
clientRunId: string;
|
||||||
@@ -130,7 +150,10 @@ export function createAgentEventHandler({
|
|||||||
timestamp: now,
|
timestamp: now,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
broadcast("chat", payload, { dropIfSlow: true });
|
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
||||||
|
if (!shouldSuppressHeartbeatBroadcast(clientRunId)) {
|
||||||
|
broadcast("chat", payload, { dropIfSlow: true });
|
||||||
|
}
|
||||||
nodeSendToSession(sessionKey, "chat", payload);
|
nodeSendToSession(sessionKey, "chat", payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,7 +181,10 @@ export function createAgentEventHandler({
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
broadcast("chat", payload);
|
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
||||||
|
if (!shouldSuppressHeartbeatBroadcast(clientRunId)) {
|
||||||
|
broadcast("chat", payload);
|
||||||
|
}
|
||||||
nodeSendToSession(sessionKey, "chat", payload);
|
nodeSendToSession(sessionKey, "chat", payload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type AgentEventPayload = {
|
|||||||
export type AgentRunContext = {
|
export type AgentRunContext = {
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
verboseLevel?: VerboseLevel;
|
verboseLevel?: VerboseLevel;
|
||||||
|
isHeartbeat?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep per-run counters so streams stay strictly monotonic per runId.
|
// Keep per-run counters so streams stay strictly monotonic per runId.
|
||||||
@@ -34,6 +35,9 @@ export function registerAgentRunContext(runId: string, context: AgentRunContext)
|
|||||||
if (context.verboseLevel && existing.verboseLevel !== context.verboseLevel) {
|
if (context.verboseLevel && existing.verboseLevel !== context.verboseLevel) {
|
||||||
existing.verboseLevel = context.verboseLevel;
|
existing.verboseLevel = context.verboseLevel;
|
||||||
}
|
}
|
||||||
|
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
|
||||||
|
existing.isHeartbeat = context.isHeartbeat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAgentRunContext(runId: string) {
|
export function getAgentRunContext(runId: string) {
|
||||||
|
|||||||
@@ -247,4 +247,58 @@ describe("resolveHeartbeatVisibility", () => {
|
|||||||
useIndicator: true,
|
useIndicator: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("webchat uses channel defaults only (no per-channel config)", () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
defaults: {
|
||||||
|
heartbeat: {
|
||||||
|
showOk: true,
|
||||||
|
showAlerts: false,
|
||||||
|
useIndicator: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const result = resolveHeartbeatVisibility({ cfg, channel: "webchat" });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
showOk: true,
|
||||||
|
showAlerts: false,
|
||||||
|
useIndicator: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("webchat returns defaults when no channel defaults configured", () => {
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const result = resolveHeartbeatVisibility({ cfg, channel: "webchat" });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
showOk: false,
|
||||||
|
showAlerts: true,
|
||||||
|
useIndicator: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("webchat ignores accountId (only uses defaults)", () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
defaults: {
|
||||||
|
heartbeat: {
|
||||||
|
showOk: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const result = resolveHeartbeatVisibility({
|
||||||
|
cfg,
|
||||||
|
channel: "webchat",
|
||||||
|
accountId: "some-account",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.showOk).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { ChannelHeartbeatVisibilityConfig } from "../config/types.channels.js";
|
import type { ChannelHeartbeatVisibilityConfig } from "../config/types.channels.js";
|
||||||
import type { DeliverableMessageChannel } from "../utils/message-channel.js";
|
import type { DeliverableMessageChannel, GatewayMessageChannel } from "../utils/message-channel.js";
|
||||||
|
|
||||||
export type ResolvedHeartbeatVisibility = {
|
export type ResolvedHeartbeatVisibility = {
|
||||||
showOk: boolean;
|
showOk: boolean;
|
||||||
@@ -14,13 +14,28 @@ const DEFAULT_VISIBILITY: ResolvedHeartbeatVisibility = {
|
|||||||
useIndicator: true, // Emit indicator events
|
useIndicator: true, // Emit indicator events
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve heartbeat visibility settings for a channel.
|
||||||
|
* Supports both deliverable channels (telegram, signal, etc.) and webchat.
|
||||||
|
* For webchat, uses channels.defaults.heartbeat since webchat doesn't have per-channel config.
|
||||||
|
*/
|
||||||
export function resolveHeartbeatVisibility(params: {
|
export function resolveHeartbeatVisibility(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
channel: DeliverableMessageChannel;
|
channel: GatewayMessageChannel;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
}): ResolvedHeartbeatVisibility {
|
}): ResolvedHeartbeatVisibility {
|
||||||
const { cfg, channel, accountId } = params;
|
const { cfg, channel, accountId } = params;
|
||||||
|
|
||||||
|
// Webchat uses channel defaults only (no per-channel or per-account config)
|
||||||
|
if (channel === "webchat") {
|
||||||
|
const channelDefaults = cfg.channels?.defaults?.heartbeat;
|
||||||
|
return {
|
||||||
|
showOk: channelDefaults?.showOk ?? DEFAULT_VISIBILITY.showOk,
|
||||||
|
showAlerts: channelDefaults?.showAlerts ?? DEFAULT_VISIBILITY.showAlerts,
|
||||||
|
useIndicator: channelDefaults?.useIndicator ?? DEFAULT_VISIBILITY.useIndicator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Layer 1: Global channel defaults
|
// Layer 1: Global channel defaults
|
||||||
const channelDefaults = cfg.channels?.defaults?.heartbeat;
|
const channelDefaults = cfg.channels?.defaults?.heartbeat;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user