FN-7097: count triage planners in running agents

Include active triage planners in live running-agent counts so global concurrency readouts match slot usage.

- Count unpaused triage tasks with planning status alongside in-progress tasks.
- Add resolver coverage for triage-only, mixed, and multi-project running-agent counts.
- Document that concurrency readouts include active triage planners and add a patch changeset.

Files changed:
 .changeset/fn-7097-triage-running-count.md         |  7 +++
 docs/dashboard-guide.md                            |  3 +-
 docs/multi-project.md                              |  3 +-
 .../src/__tests__/project-store-resolver.test.ts   | 73 ++++++++++++++++++++--
 packages/dashboard/src/project-store-resolver.ts   | 11 +++-
 5 files changed, 87 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-7097

Fusion-Task-Lineage: 705daf65-0883-40cd-b620-3be8c0f38543

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-26 23:45:03 -07:00
parent c2f8026d7a
commit f34b62cef9
5 changed files with 87 additions and 10 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: The running-agents count now includes agents actively triaging tasks, not just executors.
category: fix
dev: countRunningAgentsInStore now adds triage-column tasks with status "planning" (not paused) to the live running-agent count alongside in-progress tasks, matching the maxTriageConcurrent liveness predicate; feeds getLiveRunningAgentCounts and the global-concurrency readouts.

View File

@@ -842,6 +842,7 @@ Features:
<!-- FNXC:CommandCenter 2026-06-25-19:47: FN-7019 restored the user-facing picker contract: preset and custom date-range selections must change every historical analytics tab, while Mission Control stays live and intentionally ignores historical range filters. -->
<!-- FNXC:CommandCenter 2026-06-19-23:54: FN-6755 moved team-specific operations out of Overview: org hierarchy and heartbeat pause/resume live in Team, while Overview keeps global AI engine, concurrency, and theme controls. -->
<!-- FNXC:GlobalConcurrencyControls 2026-06-26-00:00: The Command Center Concurrency card mirrors the footer concurrency popover by showing read-only running-agent counts and current-use markers for the shared global cap and current-project max-concurrent slider. -->
<!-- FNXC:GlobalConcurrencyControls 2026-06-26-18:35: Those running-agent counts include in-progress executors and active triage planners (`triage` + `planning`, not paused) because both consume global concurrency slots. -->
<!-- FNXC:CommandCenter 2026-06-26-00:00: The four Overview Concurrency sliders change live scheduler capacity, so each settled edit opens a confirmation popup before persisting; cancel, backdrop, or Escape leaves the previous persisted value in place. -->
- **Overview controls dashboard** sits at the top of the Overview landing surface on desktop and mobile. It includes AI engine stop/start backed by `globalPause`, live scheduler status from executor stats, the shared Global Max Concurrent slider backed by `/api/global-concurrency`, range sliders for `maxConcurrent`, `maxTriageConcurrent`, and `maxWorktrees` that persist through `/api/settings`, and a compact theme dropdown with the same color-chip swatches and Shadcn variant list as Settings → Appearance. The four concurrency sliders ask for confirmation after a changed value settles; confirming persists the new cap, while cancel, backdrop, or Escape dismissal reverts to the last persisted value without saving. The global and current-project max-concurrent sliders show running-agent counts plus a current-use dot on the track once utilization data loads; triage and worktree sliders remain cap-only. These controls reuse existing APIs and App-level theme setters; they do not add a new backend route or second theme owner.
- **Overview** summarizes token usage/cost, autonomy, active nodes, sessions, agent runs, tasks done, model breadth, and real open signals, and includes the SDLC throughput funnel for the selected range at the bottom of the Overview content in loading, error, empty, and populated states. Its token total and Live activity snapshot token metric refresh on a bounded live cadence and animate number changes while preserving reduced-motion preferences. The sessions card uses the selected-range `ActivityAnalytics.sessions` value already loaded for the overview. The Live activity snapshot also shows the current board-state count for tasks in progress, independent of the selected analytics date range. Overview includes a graph-rich software-factory snapshot with the existing tokens-by-model bar, tool-category bar, real recharts token-share pie, and the daily activity multi-series line chart placed before the daily activity sparkline/trend so the richer line graph sits higher in the chart grid. These reuse the already-loaded tokens, tools, activity, and signals analytics; the signals count comes from `/api/command-center/signals` and renders unavailable (`—`) while the incidents-backed response is loading or unavailable. The chart reveal/glow accents are decorative and disabled when reduced-motion preferences are active. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range.
@@ -998,7 +999,7 @@ Use this panel when upgrading a project with pre-FN-6245/FN-6277 in-review rows
### Executor footer engine controls
The global AI engine stop/start control and triage pause/resume control live in the executor footer status bar rather than the header. Select the small engine-controls button beside the executor state badge, or select the state text such as **Running**, to open the footer popover. The popover includes **Stop AI engine** / **Start AI engine**, **Pause triage** / **Resume scheduling**, and live scheduler sliders for max concurrent tasks, max triage concurrency, and max worktrees. The global and current-project concurrency sliders also show how many agents are running and a dot on the slider track for current use, clamped to the track when usage exceeds the configured cap. Slider changes save through the existing `/api/settings` path with the same debounced behavior used by Command Center controls; no separate backend route is required.
The global AI engine stop/start control and triage pause/resume control live in the executor footer status bar rather than the header. Select the small engine-controls button beside the executor state badge, or select the state text such as **Running**, to open the footer popover. The popover includes **Stop AI engine** / **Start AI engine**, **Pause triage** / **Resume scheduling**, and live scheduler sliders for max concurrent tasks, max triage concurrency, and max worktrees. The global and current-project concurrency sliders also show how many agents are running, including actively-triaging planners (`triage` + `planning`, not paused), and a dot on the slider track for current use, clamped to the track when usage exceeds the configured cap. Slider changes save through the existing `/api/settings` path with the same debounced behavior used by Command Center controls; no separate backend route is required.
### Engine status banner

View File

@@ -112,7 +112,8 @@ Central health tracking keeps mutable project metrics, including:
## Global Concurrency Management
A singleton central record enforces system-wide limits so one project cannot monopolize all execution slots. `globalConcurrency.currentlyActive` remains persisted slot bookkeeping maintained by acquire/free flows; live read-only running-agent displays derive `currentlyActive` and per-project active counts from `in-progress` tasks in already-open project stores, while the persisted `globalMaxConcurrent` cap and `queuedCount` continue to come from central concurrency state. The slot acquire/free limiter semantics and DB column names are unchanged.
<!-- FNXC:GlobalConcurrencyControls 2026-06-26-18:35: Live global-concurrency readouts must count both in-progress executors and active triage planners because both hold concurrency slots; paused or non-planning triage rows stay excluded. -->
A singleton central record enforces system-wide limits so one project cannot monopolize all execution slots. `globalConcurrency.currentlyActive` remains persisted slot bookkeeping maintained by acquire/free flows; live read-only running-agent displays derive `currentlyActive` and per-project active counts from in-progress tasks plus triage tasks with `status === "planning"` that are not paused in already-open project stores, while the persisted `globalMaxConcurrent` cap and `queuedCount` continue to come from central concurrency state. The slot acquire/free limiter semantics and DB column names are unchanged.
## Plugin Scope in Multi-Project Mode

View File

@@ -13,6 +13,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
getOrCreateProjectStore,
countRunningAgentsInRegisteredProjectStores,
countRunningAgentsInStore,
evictProjectStore,
evictAllProjectStores,
listRegisteredProjectStores,
@@ -366,8 +367,18 @@ describe("countRunningAgentsInRegisteredProjectStores", () => {
createdStores.length = 0;
});
function installTaskList(store: TaskStore, columns: string[]) {
const tasks = columns.map((column, index) => ({ id: `FN-${index + 1}`, column }));
type SeedTask = {
column: string;
status?: string;
paused?: boolean;
};
function installTaskList(store: TaskStore, seedTasks: Array<string | SeedTask>) {
const tasks = seedTasks.map((seedTask, index) => (
typeof seedTask === "string"
? { id: `FN-${index + 1}`, column: seedTask }
: { id: `FN-${index + 1}`, ...seedTask }
));
const listTasks = vi.fn().mockImplementation(async (options?: { column?: string }) => (
options?.column ? tasks.filter((task) => task.column === options.column) : tasks
));
@@ -385,7 +396,7 @@ describe("countRunningAgentsInRegisteredProjectStores", () => {
const counts = await countRunningAgentsInRegisteredProjectStores(["proj_open", "proj_unopened"]);
expect(counts).toEqual({ proj_open: 2 });
expect(listTasks).toHaveBeenCalledWith({ column: "in-progress", slim: true });
expect(listTasks).toHaveBeenCalledWith({ slim: true });
expect(createdStores).toHaveLength(1);
expect(openEntry?.watchMock).not.toHaveBeenCalled();
});
@@ -402,7 +413,7 @@ describe("countRunningAgentsInRegisteredProjectStores", () => {
const counts = await countRunningAgentsInRegisteredProjectStores(["proj_zero", "proj_unopened"]);
expect(counts).toEqual({ proj_zero: 0 });
expect(zeroListTasks).toHaveBeenCalledWith({ column: "in-progress", slim: true });
expect(zeroListTasks).toHaveBeenCalledWith({ slim: true });
expect(ignoredListTasks).not.toHaveBeenCalled();
expect(createdStores).toHaveLength(2);
expect(zeroEntry?.watchMock).not.toHaveBeenCalled();
@@ -419,8 +430,58 @@ describe("countRunningAgentsInRegisteredProjectStores", () => {
const counts = await countRunningAgentsInRegisteredProjectStores(["proj_a", "proj_b"]);
expect(counts).toEqual({ proj_a: 1, proj_b: 2 });
expect(listTasksA).toHaveBeenCalledWith({ column: "in-progress", slim: true });
expect(listTasksB).toHaveBeenCalledWith({ column: "in-progress", slim: true });
expect(listTasksA).toHaveBeenCalledWith({ slim: true });
expect(listTasksB).toHaveBeenCalledWith({ slim: true });
expect(createdStores).toHaveLength(2);
});
it("counts an actively triaging planning task even when no executors are running", async () => {
const store = await getOrCreateProjectStore("proj_triage_only");
const listTasks = installTaskList(store, [
{ column: "triage", status: "planning" },
"todo",
]);
await expect(countRunningAgentsInStore(store)).resolves.toBe(1);
expect(listTasks).toHaveBeenCalledWith({ slim: true });
});
it("sums in-progress executors and active triage agents while excluding inactive triage states", async () => {
const store = await getOrCreateProjectStore("proj_mixed");
installTaskList(store, [
"in-progress",
{ column: "triage", status: "planning" },
{ column: "triage", status: "planning", paused: true },
{ column: "triage", status: "triaged" },
{ column: "triage" },
"todo",
]);
await expect(countRunningAgentsInStore(store)).resolves.toBe(2);
});
it("returns per-project active triage and executor counts for multiple already-open stores", async () => {
const storeA = await getOrCreateProjectStore("proj_triage_a");
const storeB = await getOrCreateProjectStore("proj_triage_b");
const listTasksA = installTaskList(storeA, [
"in-progress",
{ column: "triage", status: "planning" },
{ column: "triage", status: "waiting" },
]);
const listTasksB = installTaskList(storeB, [
{ column: "triage", status: "planning" },
{ column: "triage", status: "planning" },
{ column: "triage", status: "planning", paused: true },
"done",
]);
vi.clearAllMocks();
const counts = await countRunningAgentsInRegisteredProjectStores(["proj_triage_a", "proj_triage_b"]);
expect(counts).toEqual({ proj_triage_a: 2, proj_triage_b: 2 });
expect(Object.values(counts).reduce((sum, count) => sum + count, 0)).toBe(4);
expect(listTasksA).toHaveBeenCalledWith({ slim: true });
expect(listTasksB).toHaveBeenCalledWith({ slim: true });
expect(createdStores).toHaveLength(2);
});
});

View File

@@ -158,9 +158,16 @@ export function listRegisteredProjectStores(): Array<{ projectId: string; store:
return Array.from(storeCache.entries(), ([projectId, store]) => ({ projectId, store }));
}
/**
* FNXC:GlobalConcurrencyControls 2026-06-26-18:20:
* The live running-agent count must include actively-triaging agents because `triage` + `planning` tasks hold a global concurrency slot just like in-progress executors. Mirror the `maxTriageConcurrent` liveness predicate from `triage.ts` so the footer and Command Center do not under-count planning work.
*/
export async function countRunningAgentsInStore(store: TaskStore): Promise<number> {
const tasks = await store.listTasks({ column: "in-progress", slim: true });
return tasks.length;
const tasks = await store.listTasks({ slim: true });
return tasks.filter((task) => (
task.column === "in-progress" ||
(task.column === "triage" && task.status === "planning" && !task.paused)
)).length;
}
/**