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

@@ -1,3 +1,4 @@
import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js";
import type { RuntimeEnv } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { withProgress } from "../cli/progress.js";
@@ -120,6 +121,14 @@ export async function statusCommand(
}),
)
: undefined;
const lastHeartbeat =
opts.deep && gatewayReachable
? await callGateway<HeartbeatEventPayload | null>({
method: "last-heartbeat",
params: {},
timeoutMs: opts.timeoutMs,
}).catch(() => null)
: null;
const configChannel = normalizeUpdateChannel(cfg.update?.channel);
const channelInfo = resolveEffectiveUpdateChannel({
@@ -157,7 +166,7 @@ export async function statusCommand(
nodeService: nodeDaemon,
agents: agentStatus,
securityAudit,
...(health || usage ? { health, usage } : {}),
...(health || usage || lastHeartbeat ? { health, usage, lastHeartbeat } : {}),
},
null,
2,
@@ -275,6 +284,21 @@ export async function statusCommand(
.filter(Boolean);
return parts.length > 0 ? parts.join(", ") : "disabled";
})();
const lastHeartbeatValue = (() => {
if (!opts.deep) {
return null;
}
if (!gatewayReachable) {
return warn("unavailable");
}
if (!lastHeartbeat) {
return muted("none");
}
const age = formatAge(Date.now() - lastHeartbeat.ts);
const channel = lastHeartbeat.channel ?? "unknown";
const accountLabel = lastHeartbeat.accountId ? `account ${lastHeartbeat.accountId}` : null;
return [lastHeartbeat.status, `${age} ago`, channel, accountLabel].filter(Boolean).join(" · ");
})();
const storeLabel =
summary.sessions.paths.length > 1
@@ -371,6 +395,7 @@ export async function statusCommand(
{ Item: "Probes", Value: probesValue },
{ Item: "Events", Value: eventsValue },
{ Item: "Heartbeat", Value: heartbeatValue },
...(lastHeartbeatValue ? [{ Item: "Last heartbeat", Value: lastHeartbeatValue }] : []),
{
Item: "Sessions",
Value: `${summary.sessions.count} active · default ${defaults.model ?? "unknown"}${defaultCtx} · ${storeLabel}`,