feat(heartbeat): add configurable visibility for heartbeat responses

Add per-channel and per-account heartbeat visibility settings:
- showOk: hide/show HEARTBEAT_OK messages (default: false)
- showAlerts: hide/show alert messages (default: true)
- useIndicator: emit typing indicator events (default: true)

Config precedence: per-account > per-channel > channel-defaults > global

This allows silencing routine heartbeat acks while still surfacing
alerts when something needs attention.
This commit is contained in:
Dave Lauer
2026-01-22 10:54:07 -05:00
committed by Peter Steinberger
parent 9b12275fe1
commit f9cf508cff
18 changed files with 695 additions and 6 deletions

View File

@@ -7,8 +7,19 @@ import type { TelegramConfig } from "./types.telegram.js";
import type { WhatsAppConfig } from "./types.whatsapp.js";
import type { GroupPolicy } from "./types.base.js";
export type ChannelHeartbeatVisibilityConfig = {
/** Show HEARTBEAT_OK acknowledgments in chat (default: false). */
showOk?: boolean;
/** Show heartbeat alerts with actual content (default: true). */
showAlerts?: boolean;
/** Emit indicator events for UI status display (default: true). */
useIndicator?: boolean;
};
export type ChannelDefaultsConfig = {
groupPolicy?: GroupPolicy;
/** Default heartbeat visibility for all channels. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type ChannelsConfig = {

View File

@@ -6,6 +6,7 @@ import type {
OutboundRetryConfig,
ReplyToMode,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js";
@@ -121,6 +122,8 @@ export type DiscordAccountConfig = {
dm?: DiscordDmConfig;
/** New per-guild config keyed by guild id or slug. */
guilds?: Record<string, DiscordGuildEntry>;
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type DiscordConfig = {

View File

@@ -4,6 +4,7 @@ import type {
GroupPolicy,
MarkdownConfig,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js";
@@ -63,6 +64,8 @@ export type IMessageAccountConfig = {
tools?: GroupToolPolicyConfig;
}
>;
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type IMessageConfig = {

View File

@@ -4,6 +4,7 @@ import type {
GroupPolicy,
MarkdownConfig,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js";
@@ -94,4 +95,6 @@ export type MSTeamsConfig = {
mediaMaxMb?: number;
/** SharePoint site ID for file uploads in group chats/channels (e.g., "contoso.sharepoint.com,guid1,guid2"). */
sharePointSiteId?: string;
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};

View File

@@ -4,6 +4,7 @@ import type {
GroupPolicy,
MarkdownConfig,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js";
export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist";
@@ -63,6 +64,8 @@ export type SignalAccountConfig = {
reactionNotifications?: SignalReactionNotificationMode;
/** Allowlist for reaction notifications when mode is allowlist. */
reactionAllowlist?: Array<string | number>;
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type SignalConfig = {

View File

@@ -5,6 +5,7 @@ import type {
MarkdownConfig,
ReplyToMode,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js";
@@ -136,6 +137,8 @@ export type SlackAccountConfig = {
slashCommand?: SlackSlashCommandConfig;
dm?: SlackDmConfig;
channels?: Record<string, SlackChannelConfig>;
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type SlackConfig = {

View File

@@ -7,6 +7,7 @@ import type {
OutboundRetryConfig,
ReplyToMode,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js";
@@ -113,6 +114,8 @@ export type TelegramAccountConfig = {
* - "extensive": agent can react liberally when appropriate
*/
reactionLevel?: "off" | "ack" | "minimal" | "extensive";
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type TelegramTopicConfig = {

View File

@@ -4,6 +4,7 @@ import type {
GroupPolicy,
MarkdownConfig,
} from "./types.base.js";
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
import type { DmConfig } from "./types.messages.js";
import type { GroupToolPolicyConfig } from "./types.tools.js";
@@ -86,6 +87,8 @@ export type WhatsAppConfig = {
};
/** Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable). */
debounceMs?: number;
/** Heartbeat visibility settings for this channel. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};
export type WhatsAppAccountConfig = {
@@ -147,4 +150,6 @@ export type WhatsAppAccountConfig = {
};
/** Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable). */
debounceMs?: number;
/** Heartbeat visibility settings for this account. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
};

View File

@@ -0,0 +1,10 @@
import { z } from "zod";
export const ChannelHeartbeatVisibilitySchema = z
.object({
showOk: z.boolean().optional(),
showAlerts: z.boolean().optional(),
useIndicator: z.boolean().optional(),
})
.strict()
.optional();

View File

@@ -15,6 +15,7 @@ import {
requireOpenAllowFrom,
} from "./zod-schema.core.js";
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
import {
normalizeTelegramCommandDescription,
normalizeTelegramCommandName,
@@ -122,6 +123,7 @@ export const TelegramAccountSchemaBase = z
.optional(),
reactionNotifications: z.enum(["off", "own", "all"]).optional(),
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict();
@@ -241,6 +243,7 @@ export const DiscordAccountSchema = z
replyToMode: ReplyToModeSchema.optional(),
dm: DiscordDmSchema.optional(),
guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict();
@@ -351,6 +354,7 @@ export const SlackAccountSchema = z
.optional(),
dm: SlackDmSchema.optional(),
channels: z.record(z.string(), SlackChannelSchema.optional()).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict();
@@ -416,6 +420,7 @@ export const SignalAccountSchemaBase = z
mediaMaxMb: z.number().int().positive().optional(),
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict();
@@ -477,6 +482,7 @@ export const IMessageAccountSchemaBase = z
.optional(),
)
.optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict();
@@ -553,6 +559,7 @@ export const BlueBubblesAccountSchemaBase = z
blockStreaming: z.boolean().optional(),
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
groups: z.record(z.string(), BlueBubblesGroupConfigSchema.optional()).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict();
@@ -630,6 +637,7 @@ export const MSTeamsConfigSchema = z
mediaMaxMb: z.number().positive().optional(),
/** SharePoint site ID for file uploads in group chats/channels (e.g., "contoso.sharepoint.com,guid1,guid2") */
sharePointSiteId: z.string().optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict()
.superRefine((value, ctx) => {

View File

@@ -8,6 +8,7 @@ import {
MarkdownConfigSchema,
} from "./zod-schema.core.js";
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
export const WhatsAppAccountSchema = z
.object({
@@ -53,6 +54,7 @@ export const WhatsAppAccountSchema = z
.strict()
.optional(),
debounceMs: z.number().int().nonnegative().optional().default(0),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict()
.superRefine((value, ctx) => {
@@ -115,6 +117,7 @@ export const WhatsAppConfigSchema = z
.strict()
.optional(),
debounceMs: z.number().int().nonnegative().optional().default(0),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict()
.superRefine((value, ctx) => {

View File

@@ -11,15 +11,18 @@ import {
} from "./zod-schema.providers-core.js";
import { WhatsAppConfigSchema } from "./zod-schema.providers-whatsapp.js";
import { GroupPolicySchema } from "./zod-schema.core.js";
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
export * from "./zod-schema.providers-core.js";
export * from "./zod-schema.providers-whatsapp.js";
export { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
export const ChannelsSchema = z
.object({
defaults: z
.object({
groupPolicy: GroupPolicySchema.optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
})
.strict()
.optional(),