feat(FN-1255): add comment-aware heartbeat wakes and blocked-state dedup

- Add BlockedStateSnapshot typing/export and AgentStore persistence APIs for last blocked heartbeat state
- Deduplicate blocked-task heartbeat comments using blockedBy + context hash, and clear blocked snapshots when tasks are no longer blocked
- Thread triggeringCommentIds/triggeringCommentType through heartbeat execution, wake context, scheduler assignment triggers, and runtime wiring
- Trigger immediate heartbeat runs from task/steering comment routes for assigned immediate-response agents, with validation for comment wake fields on /api/agents/:id/runs
- Expand core, engine, and dashboard tests to cover blocked dedup logic, comment-triggered wakes, validation, and skip scenarios
This commit is contained in:
gsxdsm
2026-04-08 18:38:06 -07:00
parent 8b538988eb
commit a05cfd4b55
10 changed files with 985 additions and 9 deletions

View File

@@ -1753,6 +1753,50 @@ describe("AgentStore", () => {
});
});
// ── blocked state persistence ─────────────────────────────────────
describe("blocked state persistence", () => {
it("roundtrips last blocked state via set/get", async () => {
const agent = await store.createAgent({ name: "BlockedState", role: "executor" });
const snapshot = {
taskId: "FN-123",
blockedBy: "FN-122",
recordedAt: new Date().toISOString(),
contextHash: "abc123hash",
};
await store.setLastBlockedState(agent.id, snapshot);
const loaded = await store.getLastBlockedState(agent.id);
expect(loaded).toEqual(snapshot);
});
it("returns null when no blocked-state file exists", async () => {
const agent = await store.createAgent({ name: "NoBlockedState", role: "executor" });
const loaded = await store.getLastBlockedState(agent.id);
expect(loaded).toBeNull();
});
it("clearLastBlockedState removes persisted snapshot", async () => {
const agent = await store.createAgent({ name: "ClearBlockedState", role: "executor" });
await store.setLastBlockedState(agent.id, {
taskId: "FN-999",
blockedBy: "FN-998",
recordedAt: new Date().toISOString(),
contextHash: "will-clear",
});
await store.clearLastBlockedState(agent.id);
const loaded = await store.getLastBlockedState(agent.id);
expect(loaded).toBeNull();
expect(existsSync(join(rootDir, "agents", `${agent.id}-last-blocked.json`))).toBe(false);
});
});
// ── getAgentDetail ────────────────────────────────────────────────
describe("getAgentDetail", () => {