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>
This commit is contained in:
lsh411
2026-02-05 06:49:12 +09:00
committed by GitHub
parent bebf323775
commit a42e3cb78a
10 changed files with 267 additions and 9 deletions

View File

@@ -534,6 +534,19 @@ export async function runHeartbeatOnce(opts: {
const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat);
const previousUpdatedAt = entry?.updatedAt;
const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat });
const heartbeatAccountId = heartbeat?.accountId?.trim();
if (delivery.reason === "unknown-account") {
log.warn("heartbeat: unknown accountId", {
accountId: delivery.accountId ?? heartbeatAccountId ?? null,
target: heartbeat?.target ?? "last",
});
} else if (heartbeatAccountId) {
log.info("heartbeat: using explicit accountId", {
accountId: delivery.accountId ?? heartbeatAccountId,
target: heartbeat?.target ?? "last",
channel: delivery.channel,
});
}
const visibility =
delivery.channel !== "none"
? resolveHeartbeatVisibility({
@@ -569,6 +582,7 @@ export async function runHeartbeatOnce(opts: {
reason: "alerts-disabled",
durationMs: Date.now() - startedAt,
channel: delivery.channel !== "none" ? delivery.channel : undefined,
accountId: delivery.accountId,
});
return { status: "skipped", reason: "alerts-disabled" };
}
@@ -626,6 +640,7 @@ export async function runHeartbeatOnce(opts: {
reason: opts.reason,
durationMs: Date.now() - startedAt,
channel: delivery.channel !== "none" ? delivery.channel : undefined,
accountId: delivery.accountId,
silent: !okSent,
indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-empty") : undefined,
});
@@ -659,6 +674,7 @@ export async function runHeartbeatOnce(opts: {
reason: opts.reason,
durationMs: Date.now() - startedAt,
channel: delivery.channel !== "none" ? delivery.channel : undefined,
accountId: delivery.accountId,
silent: !okSent,
indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-token") : undefined,
});
@@ -695,6 +711,7 @@ export async function runHeartbeatOnce(opts: {
durationMs: Date.now() - startedAt,
hasMedia: false,
channel: delivery.channel !== "none" ? delivery.channel : undefined,
accountId: delivery.accountId,
});
return { status: "ran", durationMs: Date.now() - startedAt };
}
@@ -714,6 +731,7 @@ export async function runHeartbeatOnce(opts: {
preview: previewText?.slice(0, 200),
durationMs: Date.now() - startedAt,
hasMedia: mediaUrls.length > 0,
accountId: delivery.accountId,
});
return { status: "ran", durationMs: Date.now() - startedAt };
}
@@ -731,6 +749,7 @@ export async function runHeartbeatOnce(opts: {
durationMs: Date.now() - startedAt,
channel: delivery.channel,
hasMedia: mediaUrls.length > 0,
accountId: delivery.accountId,
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
});
return { status: "ran", durationMs: Date.now() - startedAt };
@@ -752,6 +771,7 @@ export async function runHeartbeatOnce(opts: {
durationMs: Date.now() - startedAt,
hasMedia: mediaUrls.length > 0,
channel: delivery.channel,
accountId: delivery.accountId,
});
log.info("heartbeat: channel not ready", {
channel: delivery.channel,
@@ -801,6 +821,7 @@ export async function runHeartbeatOnce(opts: {
durationMs: Date.now() - startedAt,
hasMedia: mediaUrls.length > 0,
channel: delivery.channel,
accountId: delivery.accountId,
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
});
return { status: "ran", durationMs: Date.now() - startedAt };
@@ -811,6 +832,7 @@ export async function runHeartbeatOnce(opts: {
reason,
durationMs: Date.now() - startedAt,
channel: delivery.channel !== "none" ? delivery.channel : undefined,
accountId: delivery.accountId,
indicatorType: visibility.useIndicator ? resolveIndicatorType("failed") : undefined,
});
log.error(`heartbeat failed: ${reason}`, { error: reason });