fix(heartbeat): report the cadence of active agent heartbeats (#126869)

This commit is contained in:
Peter Steinberger
2026-08-20 15:48:19 -07:00
committed by GitHub
parent b00734bd4c
commit 40e3ab8784
4 changed files with 82 additions and 30 deletions
@@ -107,6 +107,7 @@ describe("collectGatewayHealthSnapshot legacy owner projection", () => {
expect(explicit.defaultAgentId).toBeUndefined();
expect(explicit.agents.every((agent) => !agent.isDefault)).toBe(true);
expect(explicit.agents.every((agent) => !agent.heartbeat.enabled)).toBe(true);
expect(explicit.heartbeatSeconds).toBe(0);
});
it("projects the configured heartbeat owner's cadence", async () => {
@@ -129,4 +130,34 @@ describe("collectGatewayHealthSnapshot legacy owner projection", () => {
);
expect(health.heartbeatSeconds).toBe(5 * 60);
});
it.each([
{ label: "an earlier agent", heartbeatAgentId: undefined },
{ label: "the configured owner", heartbeatAgentId: "ops" },
])(
"reports the active heartbeat when $label disables its cadence",
async ({ heartbeatAgentId }) => {
testConfig = {
agents: {
ownership: "explicit",
defaults: {
heartbeat: {
every: "30m",
...(heartbeatAgentId ? { agentId: heartbeatAgentId } : {}),
},
},
entries: {
ops: { heartbeat: { every: "0m" } },
research: { heartbeat: { every: "1h" } },
},
},
};
const health = await collectGatewayHealthSnapshot({ audience: "admin", probe: false });
expect(health.agents.map((agent) => agent.agentId)).toEqual(["ops", "research"]);
expect(health.agents.map((agent) => agent.heartbeat.enabled)).toEqual([false, true]);
expect(health.heartbeatSeconds).toBe(60 * 60);
},
);
});
+5 -1
View File
@@ -232,7 +232,11 @@ export async function collectGatewayHealthSnapshot(params: {
);
const heartbeatSummaryAgent =
(configuredHeartbeatAgentId
? agents.find((agent) => agent.agentId === normalizeAgentId(configuredHeartbeatAgentId))
? agents.find(
(agent) =>
agent.heartbeat.enabled &&
agent.agentId === normalizeAgentId(configuredHeartbeatAgentId),
)
: undefined) ??
agents.find((agent) => agent.heartbeat.enabled) ??
summaryAgent;
@@ -375,6 +375,43 @@ describe("resolveHeartbeatIntervalMs", () => {
expect(resolveHeartbeatSummaryForAgent(cfg, "main").session).toBe("telegram:alerts");
});
it.each([
{
label: "global",
cfg: {
agents: {
defaults: {
heartbeat: { every: "0m", target: "last", session: "telegram:default" },
},
},
},
session: "telegram:default",
},
{
label: "per-agent",
cfg: {
agents: {
defaults: {
heartbeat: { every: "30m", target: "last", session: "telegram:default" },
},
list: [{ id: "main", heartbeat: { every: "0m", session: "telegram:alerts" } }],
},
},
session: "telegram:alerts",
},
] satisfies Array<{ label: string; cfg: OpenClawConfig; session: string }>)(
"reports a disabled $label heartbeat as disabled",
({ cfg, session }) => {
expect(resolveHeartbeatSummaryForAgent(cfg, "main")).toMatchObject({
enabled: false,
every: "disabled",
everyMs: null,
target: "last",
session,
});
},
);
it("returns default when unset", () => {
expect(resolveHeartbeatIntervalMs({})).toBe(30 * 60_000);
});
+9 -29
View File
@@ -97,37 +97,17 @@ export function resolveHeartbeatSummaryForAgent(
const defaults = cfg.agents?.defaults?.heartbeat;
const overrides = agentId ? resolveAgentConfig(cfg, agentId)?.heartbeat : undefined;
const merged = defaults || overrides ? { ...defaults, ...overrides } : undefined;
const enabled = isHeartbeatEnabledForAgent(cfg, agentId);
if (!enabled) {
return {
enabled: false,
every: "disabled",
everyMs: null,
prompt: resolveHeartbeatPromptText(merged?.prompt),
target: merged?.target ?? DEFAULT_HEARTBEAT_TARGET,
model: merged?.model,
session: merged?.session,
ackMaxChars: DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
};
}
const every = merged?.every ?? DEFAULT_HEARTBEAT_EVERY;
const everyMs = resolveHeartbeatIntervalMs(cfg, undefined, merged);
const prompt = resolveHeartbeatPromptText(merged?.prompt);
const target = merged?.target ?? DEFAULT_HEARTBEAT_TARGET;
const model = merged?.model;
const session = merged?.session;
const ackMaxChars = DEFAULT_HEARTBEAT_ACK_MAX_CHARS;
const enabled = isHeartbeatEnabledForAgent(cfg, agentId) && everyMs !== null;
return {
enabled: true,
every,
everyMs,
prompt,
target,
model,
session,
ackMaxChars,
enabled,
every: enabled ? (merged?.every ?? DEFAULT_HEARTBEAT_EVERY) : "disabled",
everyMs: enabled ? everyMs : null,
prompt: resolveHeartbeatPromptText(merged?.prompt),
target: merged?.target ?? DEFAULT_HEARTBEAT_TARGET,
model: merged?.model,
session: merged?.session,
ackMaxChars: DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
};
}