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

@@ -8,6 +8,7 @@ import type {
} from "../../utils/message-channel.js";
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { deliveryContextFromSession } from "../../utils/delivery-context.js";
import {
INTERNAL_MESSAGE_CHANNEL,
@@ -207,11 +208,37 @@ export function resolveHeartbeatDeliveryTarget(params: {
mode: "heartbeat",
});
const heartbeatAccountId = heartbeat?.accountId?.trim();
// Use explicit accountId from heartbeat config if provided, otherwise fall back to session
let effectiveAccountId = heartbeatAccountId || resolvedTarget.accountId;
if (heartbeatAccountId && resolvedTarget.channel) {
const plugin = getChannelPlugin(resolvedTarget.channel);
const listAccountIds = plugin?.config.listAccountIds;
const accountIds = listAccountIds ? listAccountIds(cfg) : [];
if (accountIds.length > 0) {
const normalizedAccountId = normalizeAccountId(heartbeatAccountId);
const normalizedAccountIds = new Set(
accountIds.map((accountId) => normalizeAccountId(accountId)),
);
if (!normalizedAccountIds.has(normalizedAccountId)) {
return {
channel: "none",
reason: "unknown-account",
accountId: normalizedAccountId,
lastChannel: resolvedTarget.lastChannel,
lastAccountId: resolvedTarget.lastAccountId,
};
}
effectiveAccountId = normalizedAccountId;
}
}
if (!resolvedTarget.channel || !resolvedTarget.to) {
return {
channel: "none",
reason: "no-target",
accountId: resolvedTarget.accountId,
accountId: effectiveAccountId,
lastChannel: resolvedTarget.lastChannel,
lastAccountId: resolvedTarget.lastAccountId,
};
@@ -221,14 +248,14 @@ export function resolveHeartbeatDeliveryTarget(params: {
channel: resolvedTarget.channel,
to: resolvedTarget.to,
cfg,
accountId: resolvedTarget.accountId,
accountId: effectiveAccountId,
mode: "heartbeat",
});
if (!resolved.ok) {
return {
channel: "none",
reason: "no-target",
accountId: resolvedTarget.accountId,
accountId: effectiveAccountId,
lastChannel: resolvedTarget.lastChannel,
lastAccountId: resolvedTarget.lastAccountId,
};
@@ -241,7 +268,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
channel: resolvedTarget.channel,
to: resolvedTarget.to,
cfg,
accountId: resolvedTarget.accountId,
accountId: effectiveAccountId,
mode: "explicit",
});
if (explicit.ok && explicit.to !== resolved.to) {
@@ -253,7 +280,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
channel: resolvedTarget.channel,
to: resolved.to,
reason,
accountId: resolvedTarget.accountId,
accountId: effectiveAccountId,
lastChannel: resolvedTarget.lastChannel,
lastAccountId: resolvedTarget.lastAccountId,
};
@@ -301,11 +328,13 @@ export function resolveHeartbeatSenderContext(params: {
}): HeartbeatSenderContext {
const provider =
params.delivery.channel !== "none" ? params.delivery.channel : params.delivery.lastChannel;
const accountId =
params.delivery.accountId ??
(provider === params.delivery.lastChannel ? params.delivery.lastAccountId : undefined);
const allowFrom = provider
? (getChannelPlugin(provider)?.config.resolveAllowFrom?.({
cfg: params.cfg,
accountId:
provider === params.delivery.lastChannel ? params.delivery.lastAccountId : undefined,
accountId,
}) ?? [])
: [];