Files
openclaw/src/infra/heartbeat-events.ts
lsh411 a42e3cb78a feat(heartbeat): add accountId config option for multi-agent routing (#8702)
* feat(heartbeat): add accountId config option for multi-agent routing

Add optional accountId field to heartbeat configuration, allowing
multi-agent setups to explicitly specify which Telegram account
should be used for heartbeat delivery.

Previously, heartbeat delivery would use the accountId from the
session's deliveryContext. When a session had no prior conversation
history, heartbeats would default to the first/primary account
instead of the agent's intended bot.

Changes:
- Add accountId to HeartbeatSchema (zod-schema.agent-runtime.ts)
- Use heartbeat.accountId with fallback to session accountId (targets.ts)

Backward compatible: if accountId is not specified, behavior is unchanged.

Closes #8695

* fix: improve heartbeat accountId routing (#8702) (thanks @lsh411)

* fix: harden heartbeat accountId routing (#8702) (thanks @lsh411)

* fix: expose heartbeat accountId in status (#8702) (thanks @lsh411)

* chore: format status + heartbeat tests (#8702) (thanks @lsh411)

---------

Co-authored-by: m1 16 512 <m116512@m1ui-MacBookAir-2.local>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
2026-02-04 16:49:12 -05:00

59 lines
1.5 KiB
TypeScript

export type HeartbeatIndicatorType = "ok" | "alert" | "error";
export type HeartbeatEventPayload = {
ts: number;
status: "sent" | "ok-empty" | "ok-token" | "skipped" | "failed";
to?: string;
accountId?: string;
preview?: string;
durationMs?: number;
hasMedia?: boolean;
reason?: string;
/** The channel this heartbeat was sent to. */
channel?: string;
/** Whether the message was silently suppressed (showOk: false). */
silent?: boolean;
/** Indicator type for UI status display. */
indicatorType?: HeartbeatIndicatorType;
};
export function resolveIndicatorType(
status: HeartbeatEventPayload["status"],
): HeartbeatIndicatorType | undefined {
switch (status) {
case "ok-empty":
case "ok-token":
return "ok";
case "sent":
return "alert";
case "failed":
return "error";
case "skipped":
return undefined;
}
}
let lastHeartbeat: HeartbeatEventPayload | null = null;
const listeners = new Set<(evt: HeartbeatEventPayload) => void>();
export function emitHeartbeatEvent(evt: Omit<HeartbeatEventPayload, "ts">) {
const enriched: HeartbeatEventPayload = { ts: Date.now(), ...evt };
lastHeartbeat = enriched;
for (const listener of listeners) {
try {
listener(enriched);
} catch {
/* ignore */
}
}
}
export function onHeartbeatEvent(listener: (evt: HeartbeatEventPayload) => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function getLastHeartbeatEvent(): HeartbeatEventPayload | null {
return lastHeartbeat;
}