feat(FN-4255): complete Step 4 — add docs and release note

Fusion-Task-Id: FN-4255
Fusion-Task-Lineage: 315c5491-0e27-46cb-8474-fda9bb3a0eff
This commit is contained in:
Fusion
2026-05-12 23:46:48 -07:00
committed by gsxdsm
parent e985a04ce2
commit 8240a29551
3 changed files with 13 additions and 6 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fixes direct-report health classification in heartbeat report summaries to use each report's configured heartbeat interval instead of heartbeat timeout budget. Reports are now marked stale only when heartbeat age exceeds `max(heartbeatIntervalMs × 4, 5 minutes)`, matching the dashboard health semantics and preventing false stale flags for agents still within their scheduled cadence.

View File

@@ -781,6 +781,8 @@ When the bound task is `executor-class` or `blocked`, the default procedure dire
The manager-facing reports health block in that prompt is populated from `AgentStore.getAgentsByReportsTo(agent.id)`. Engine code must call that store method with its `AgentStore` instance binding intact because some implementations resolve direct reports through `this.listAgents()`. If the section disappears unexpectedly, look for logs like `Failed to load reports ... Cannot read properties of undefined (reading 'listAgents')`, which indicate an unbound method call regressed.
Direct-report staleness in this reports-health block uses each report's configured heartbeat interval, with threshold `max(heartbeatIntervalMs × 4, 5 minutes)`. This matches dashboard health classification semantics and avoids timeout-budget-based false stale flags.
This behavior is inherited by new non-ephemeral agents because agent creation seeds a per-agent `HEARTBEAT.md` file from the built-in default. If an agent sets `heartbeatProcedurePath`, that markdown file fully replaces the built-in default at runtime for task-scoped heartbeats. No-task heartbeats always fall back to the ambient built-in procedure so the prompt never references task-only tools.
For pre-existing agents, use `POST /api/agents/:id/upgrade-heartbeat-procedure` (also exposed as **Upgrade to Default Heartbeat Procedure** in the agent detail Config tab) to re-seed from the current built-in constant. When the built-in default changes, running this upgrade propagates the new default to existing agents; direct operator edits to an agents existing procedure file are preserved unless this upgrade is run (the upgrade overwrites the per-agent file).

View File

@@ -227,7 +227,7 @@ describe("executeHeartbeat", () => {
vi.mocked(store.getCachedAgent).mockImplementation((id: string) => ({
id,
runtimeConfig: { heartbeatIntervalMs: 60 * 60_000 },
}) as Agent);
}) as unknown as Agent);
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([
{ id: "agent-frontend", name: "Frontend Engineer", state: "active", taskId: "FN-201", lastHeartbeatAt: new Date(now - 53 * 60_000).toISOString(), updatedAt: new Date(now - 53 * 60_000).toISOString() } as Agent,
{ id: "agent-writer", name: "Technical Writer", state: "active", taskId: "FN-202", lastHeartbeatAt: new Date(now - 51 * 60_000).toISOString(), updatedAt: new Date(now - 51 * 60_000).toISOString() } as Agent,
@@ -250,7 +250,7 @@ describe("executeHeartbeat", () => {
vi.mocked(store.getCachedAgent).mockImplementation((id: string) => ({
id,
runtimeConfig: { heartbeatIntervalMs: 60 * 60_000 },
}) as Agent);
}) as unknown as Agent);
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([
{
id: "agent-overdue-a",
@@ -282,13 +282,13 @@ describe("executeHeartbeat", () => {
const store = createStoreWithAgentForExec();
vi.mocked(store.getCachedAgent).mockImplementation((id: string) => {
if (id === "agent-short") {
return { id, runtimeConfig: { heartbeatIntervalMs: 5 * 60_000 } } as Agent;
return { id, runtimeConfig: { heartbeatIntervalMs: 5 * 60_000 } } as unknown as Agent;
}
if (id === "agent-medium") {
return { id, runtimeConfig: { heartbeatIntervalMs: 60 * 60_000 } } as Agent;
return { id, runtimeConfig: { heartbeatIntervalMs: 60 * 60_000 } } as unknown as Agent;
}
if (id === "agent-long") {
return { id, runtimeConfig: { heartbeatIntervalMs: 4 * 60 * 60_000 } } as Agent;
return { id, runtimeConfig: { heartbeatIntervalMs: 4 * 60 * 60_000 } } as unknown as Agent;
}
return null;
});
@@ -311,7 +311,7 @@ describe("executeHeartbeat", () => {
vi.mocked(store.getCachedAgent).mockImplementation((id: string) => ({
id,
runtimeConfig: { heartbeatIntervalMs: 1_000 },
}) as Agent);
}) as unknown as Agent);
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([
{ id: "agent-fast", name: "Fast Poller", state: "active", taskId: "FN-210", lastHeartbeatAt: new Date(now - 2 * 60_000).toISOString(), updatedAt: new Date(now - 2 * 60_000).toISOString() } as Agent,
]);