feat(FN-2943): merge fusion/fn-2943

- **Active Agents panel overhaul**: Hoisted above the main agent list with a new `ActiveAgentsPanel.css` stylesheet; shows next heartbeat ETA and prevents cards from getting stuck in "Connecting..." state
- **Node routing policy**: Scheduler now enforces unavailable-node scheduling rules and routes tasks to eligible nodes more reliably (`packages/engine/src/scheduler.ts`, `node-routing-policy.ts`)
- **Task card merge timer**: Keeps the card timer live while a task is actively merging rather than resetting on merge start
- **Settings modal scroll**: Scrolls to top when auth actions succeed, with regression tests added
- **Task steps reset**: Resets steps when reopening a task that returns to todo or triage (FN-2966)
- **Core store additions**: New `packages/core/src/store.ts` test coverage for task state mutations
- **Dashboard SSE bus**: New `sse-bus.ts` for event bus communication in the dashboard
- **Version bumps**: `v0.9.0` release across all packages; `cli-alias`, `dashboard`, `engine`, `plugin-sdk`, and plugin changelogs updated
- **Changeset cleanup**: Removed 20 stale changeset files consumed by the v0.9.0 release; added two new ones for Active Agents panel fixes

Commits merged:
- test(FN-2943): complete Step 3 — add auth scroll-to-top regressions
- feat(FN-2943): complete Step 2 — scroll on successful auth actions
- feat(FN-2943): complete Step 1 — add settings content scroll ref
- feat(FN-2951): enforce unavailable-node scheduling and improve active agents panel
- chore: add changeset for Active Agents panel hoist + heartbeat ETA
- feat(dashboard): hoist Active Agents panel above main list, show next heartbeat
- fix(dashboard): add top margin between agent list and Active Agents panel
- fix(dashboard): unstick Active Agents panel cards from "Connecting..."
- chore(release): v0.9.0
- chore: add changeset for live merge timer on task card
- fix(dashboard): keep card timer live while task is actively merging
- feat(FN-2966): reset task steps when reopening to todo or triage

Files changed:
.changeset/active-agents-live-status-and-logs.md   |  13 -
 .changeset/active-agents-no-stuck-connecting.md    |  13 +
 .changeset/active-agents-panel-hoist-heartbeat.md  |  13 +
 .changeset/dashboard-merge-base-diff.md            |  13 -
 .changeset/dashboard-task-changed-files-only.md    |  13 -
 .changeset/engine-concurrency-races.md             |  13 -
 .changeset/fix-agent-heartbeat-scheduling.md       |   5 -
 .changeset/memory-dream-session-state.md           |  13 -
 .changeset/merger-ai-summarized-commits.md         |  13 -
 .changeset/multi-node-routing.md                   |  21 -
 .changeset/pluggable-notification-providers.md     |   4 -
 .changeset/pr-mode-push-branch-and-cleanup.md      |  16 -
 .../recover-mergeable-respects-merge-settings.md   |  13 -
 .changeset/remote-tailscale-qr-tui.md              |   7 -
 .changeset/require-pr-approval-setting.md          |  17 -
 .changeset/respec-from-in-review.md                |  13 -
 .changeset/restore-task-card-fallbacks.md          |  13 -
 .changeset/tui-log-copy-feedback.md                |  13 -
 .changeset/tui-wide-stats-stack.md                 |  13 -
 CHANGELOG.md                                       | 449 +++++++++++++++++++++
 package.json                                       |   2 +-
 packages/cli-alias/CHANGELOG.md                    |  60 +++
 packages/cli-alias/package.json                    |   2 +-
 packages/cli/CHANGELOG.md                          |  44 ++
 packages/cli/package.json                          |   2 +-
 packages/core/CHANGELOG.md                         |  41 ++
 packages/core/package.json                         |   2 +-
 packages/core/src/__tests__/store.test.ts          | 127 ++++++
 packages/core/src/store.ts                         |  35 +-
 packages/dashboard/CHANGELOG.md                    |  63 +++
 .../dashboard/app/components/ActiveAgentsPanel.css |  29 ++
 .../dashboard/app/components/ActiveAgentsPanel.tsx |  35 +-
 packages/dashboard/app/components/AgentsView.tsx   |   5 +-
 .../dashboard/app/components/SettingsModal.tsx     |  13 +-
 packages/dashboard/app/components/TaskCard.tsx     |  30 +-
 .../__tests__/ActiveAgentsPanel.test.tsx           |  42 ++
 .../app/components/__tests__/AgentsView.test.tsx   |  11 +-
 .../components/__tests__/SettingsModal.test.tsx    |  68 ++++
 .../app/components/__tests__/TaskCard.test.tsx     |  36 ++
 packages/dashboard/app/sse-bus.ts                  |  13 +
 packages/dashboard/package.json                    |   2 +-
 packages/desktop/CHANGELOG.md                      |  41 ++
 packages/desktop/package.json                      |   2 +-
 packages/engine/CHANGELOG.md                       |  59 +++
 packages/engine/package.json                       |   2 +-
 .../src/__tests__/node-routing-policy.test.ts      | 102 +++--
 .../src/__tests__/scheduler-node-routing.test.ts   |  43 +-
 packages/engine/src/index.ts                       |   1 +
 packages/engine/src/node-routing-policy.ts         |  41 +-
 packages/engine/src/scheduler.ts                   |  48 +--
 packages/mobile/CHANGELOG.md                       |  41 ++
 packages/mobile/package.json                       |   2 +-
 packages/pi-claude-cli/CHANGELOG.md                |  41 ++
 packages/pi-claude-cli/package.json                |   2 +-
 packages/plugin-sdk/CHANGELOG.md                   |  57 +++
 packages/plugin-sdk/package.json                   |   2 +-
 .../examples/fusion-plugin-auto-label/CHANGELOG.md |  21 +
 .../examples/fusion-plugin-auto-label/package.json |   2 +-
 .../examples/fusion-plugin-ci-status/CHANGELOG.md  |  21 +
 .../examples/fusion-plugin-ci-status/package.json  |   2 +-
 .../fusion-plugin-notification/CHANGELOG.md        |  21 +
 .../fusion-plugin-notification/package.json        |   2 +-
 .../fusion-plugin-settings-demo/CHANGELOG.md       |  21 +
 .../fusion-plugin-settings-demo/package.json       |   2 +-
 plugins/fusion-plugin-hermes-runtime/CHANGELOG.md  |  21 +
 plugins/fusion-plugin-hermes-runtime/package.json  |   2 +-
 .../fusion-plugin-openclaw-runtime/CHANGELOG.md    |  21 +
 .../fusion-plugin-openclaw-runtime/package.json    |   2 +-
 .../fusion-plugin-paperclip-runtime/CHANGELOG.md   |  21 +
 .../fusion-plugin-paperclip-runtime/package.json   |   2 +-
 70 files changed, 1617 insertions(+), 378 deletions(-)

Fusion-Task-Id: FN-2943
This commit is contained in:
Fusion
2026-04-29 14:42:30 -07:00
committed by gsxdsm
parent 8f057a75d0
commit 8793d4761e
7 changed files with 425 additions and 60 deletions

View File

@@ -2,6 +2,9 @@
// See FN-2142 for the rationale.
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
HeartbeatMonitor,
HeartbeatTriggerScheduler,
@@ -1407,6 +1410,83 @@ describe("HeartbeatMonitor", () => {
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({ reason: "task_not_found", taskId: "FN-MISSING" });
});
it("clears archived task assignments and falls back to a no-task heartbeat for identity agents", async () => {
const appendAgentLog = vi.fn().mockResolvedValue(undefined);
mockTaskStore = createMockTaskStore({
appendAgentLog,
getTask: vi.fn().mockResolvedValue({
id: "FN-ARCHIVED",
title: "Archived Task",
description: "Archived task description",
prompt: "# Archived\n\nTask is archived",
steps: [],
column: "archived",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
});
const store = createStoreWithAgentForExec({
taskId: "FN-ARCHIVED",
soul: "Monitor the project and handle ambient work.",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect((store.assignTask as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
"agent-001",
undefined,
expect.objectContaining({ agentId: "agent-001", source: "on_demand" }),
);
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const toolNames = mockedCreateFnAgent.mock.calls[0]![0]!.customTools!.map((tool: any) => tool.name);
expect(toolNames).not.toContain("fn_task_log");
expect(toolNames).not.toContain("fn_task_document_write");
expect(toolNames).not.toContain("fn_task_document_read");
expect(appendAgentLog).not.toHaveBeenCalled();
});
it("exits gracefully for explicit terminal task overrides that are not the agent's current assignment", async () => {
mockTaskStore = createMockTaskStore({
getTask: vi.fn().mockResolvedValue({
id: "FN-DONE",
title: "Done Task",
description: "Done task description",
prompt: "# Done\n\nTask is done",
steps: [],
column: "done",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
});
const store = createStoreWithAgentForExec({
taskId: "FN-LIVE",
soul: "Stay helpful.",
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
taskId: "FN-DONE",
});
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({ reason: "terminal_task", taskId: "FN-DONE", column: "done" });
expect(store.assignTask).not.toHaveBeenCalledWith("agent-001", undefined, expect.anything());
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
});
// ── Identity Agents Without Tasks ─────────────────────────────────────────────
@@ -2728,6 +2808,40 @@ describe("HeartbeatMonitor", () => {
expect(callArgs.systemPrompt).toContain("## Project Memory");
});
it("includes markdown instructions files plus soul in heartbeat system prompts", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "heartbeat-agent-instructions-"));
writeFileSync(
join(rootDir, "heartbeat-agent.md"),
"# Heartbeat Playbook\n\nCheck messages first, then create focused follow-up tasks.",
);
try {
const store = createStoreWithAgentForExec({
instructionsPath: "heartbeat-agent.md",
soul: "Operate like a calm, systems-minded operator.",
});
const taskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ memoryEnabled: false }),
} as Partial<TaskStore>);
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore, rootDir });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
expect(callArgs.systemPrompt).toContain("## Soul");
expect(callArgs.systemPrompt).toContain("Operate like a calm, systems-minded operator.");
expect(callArgs.systemPrompt).toContain("# Heartbeat Playbook");
expect(callArgs.systemPrompt).toContain("create focused follow-up tasks");
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
it("omits memory tools and instructions when project memory is disabled", async () => {
const store = createStoreWithAgentForExec();
const taskStore = createMockTaskStore({