FN-7083: derive project in-flight counts from live tasks
Derive CLI project in-flight agent displays from live in-progress task counts instead of persisted health bookkeeping. - Add shared CLI display health handling for project list and show output. - Cover stale, missing, unreadable, table, and JSON project health cases in CLI tests. - Clarify central health and concurrency fields as persisted bookkeeping in docs and types. - Add a patch changeset for the published CLI behavior fix. Files changed: .changeset/fn-7083-cli-inflight-live-count.md | 7 + docs/architecture.md | 1 + docs/multi-project.md | 4 +- .../cli/src/commands/__tests__/project.test.ts | 172 ++++++++++++++++++++- packages/cli/src/commands/project.ts | 60 ++++--- packages/cli/src/project-resolver.ts | 4 + packages/core/src/types.ts | 12 +- 7 files changed, 235 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-7083 Fusion-Task-Lineage: 2b57cb35-383f-443b-8bf3-3fc3c7a1ad43 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7083-cli-inflight-live-count.md
Normal file
7
.changeset/fn-7083-cli-inflight-live-count.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: fn project list/info now show live running-agent counts from in-progress tasks.
|
||||||
|
category: fix
|
||||||
|
dev: CLI In-Flight Agents derives from `column === "in-progress"` task counts, mirroring FN-7080's dashboard route; persisted `projectHealth.inFlightAgentCount` and slot semantics are unchanged.
|
||||||
@@ -1172,6 +1172,7 @@ SQLite schema is initialized in `packages/core/src/db.ts` and uses:
|
|||||||
- **Central DB**: `~/.fusion/fusion-central.db`
|
- **Central DB**: `~/.fusion/fusion-central.db`
|
||||||
- Schema in `packages/core/src/central-db.ts`
|
- Schema in `packages/core/src/central-db.ts`
|
||||||
- `projects`, `projectHealth`, `centralActivityLog`, `globalConcurrency`, `nodes`, `peerNodes`, `projectNodePathMappings`, `settingsSyncState`, `__meta`
|
- `projects`, `projectHealth`, `centralActivityLog`, `globalConcurrency`, `nodes`, `peerNodes`, `projectNodePathMappings`, `settingsSyncState`, `__meta`
|
||||||
|
- `projectHealth.inFlightAgentCount` and `globalConcurrency.currentlyActive` are persisted slot/health bookkeeping fields. They are not live read-layer running-agent counts; dashboard and CLI read surfaces derive current running agents from tasks in `column === "in-progress"` while leaving slot acquire/free semantics and DB column names unchanged.
|
||||||
|
|
||||||
### Memory files
|
### Memory files
|
||||||
- OpenClaw-style memory workspace:
|
- OpenClaw-style memory workspace:
|
||||||
|
|||||||
@@ -108,9 +108,11 @@ Central health tracking keeps mutable project metrics, including:
|
|||||||
- project status (`initializing`, `active`, `paused`, `errored`)
|
- project status (`initializing`, `active`, `paused`, `errored`)
|
||||||
- dashboard project status badges degrade gracefully if registry or health data briefly carries an unknown or missing status value
|
- dashboard project status badges degrade gracefully if registry or health data briefly carries an unknown or missing status value
|
||||||
|
|
||||||
|
`projectHealth.inFlightAgentCount` is persisted slot/health bookkeeping, not an authoritative live running-agent count. Read-layer surfaces that need the current number of running agents (for example the dashboard project health route and `fn project list/info`) derive it from project tasks whose `column === "in-progress"` while preserving the stored health row for non-count metadata.
|
||||||
|
|
||||||
## Global Concurrency Management
|
## Global Concurrency Management
|
||||||
|
|
||||||
A singleton central record enforces system-wide limits so one project cannot monopolize all execution slots. Slot acquire/release bookkeeping remains separate from read-only running-agent displays: live read surfaces 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.
|
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.
|
||||||
|
|
||||||
## Plugin Scope in Multi-Project Mode
|
## Plugin Scope in Multi-Project Mode
|
||||||
|
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ describe("project commands", () => {
|
|||||||
totalTasksFailed: 1,
|
totalTasksFailed: 1,
|
||||||
lastActivityAt: new Date().toISOString(),
|
lastActivityAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
mockTaskStoreListTasks.mockResolvedValue([]);
|
mockTaskStoreListTasks.mockResolvedValue([{ id: "FN-003", column: "in-progress" }]);
|
||||||
|
|
||||||
const { runProjectShow } = await import("../project.js");
|
const { runProjectShow } = await import("../project.js");
|
||||||
await runProjectShow("proj-1");
|
await runProjectShow("proj-1");
|
||||||
@@ -359,6 +359,176 @@ describe("project commands", () => {
|
|||||||
expect(output).toContain("Completed: 10");
|
expect(output).toContain("Completed: 10");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runProjectShow derives In-Flight Agents from live in-progress tasks when central health is stale", async () => {
|
||||||
|
mockGetProject.mockResolvedValue({
|
||||||
|
id: "proj-1",
|
||||||
|
name: "demo",
|
||||||
|
path: "/tmp/demo",
|
||||||
|
status: "active",
|
||||||
|
isolationMode: "in-process",
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
mockGetSettings.mockResolvedValue({});
|
||||||
|
const staleHealth = {
|
||||||
|
projectId: "proj-1",
|
||||||
|
status: "active",
|
||||||
|
activeTaskCount: 2,
|
||||||
|
inFlightAgentCount: 0,
|
||||||
|
totalTasksCompleted: 10,
|
||||||
|
totalTasksFailed: 1,
|
||||||
|
lastActivityAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
mockGetProjectHealth.mockResolvedValue(staleHealth);
|
||||||
|
mockTaskStoreListTasks.mockResolvedValue([
|
||||||
|
{ id: "FN-001", column: "todo" },
|
||||||
|
{ id: "FN-002", column: "in-progress" },
|
||||||
|
{ id: "FN-003", column: "in-progress" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { runProjectShow } = await import("../project.js");
|
||||||
|
await runProjectShow("proj-1");
|
||||||
|
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||||
|
expect(output).toContain("In-Flight Agents: 2");
|
||||||
|
expect(staleHealth.inFlightAgentCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runProjectList JSON derives health.inFlightAgentCount from live in-progress tasks", async () => {
|
||||||
|
mockListProjects.mockResolvedValue([
|
||||||
|
{ id: "proj-1", name: "app-one", path: "/tmp/app-one", status: "active", isolationMode: "in-process" },
|
||||||
|
]);
|
||||||
|
mockGetSettings.mockResolvedValue({});
|
||||||
|
mockGetProjectHealth.mockResolvedValue({
|
||||||
|
projectId: "proj-1",
|
||||||
|
status: "active",
|
||||||
|
activeTaskCount: 1,
|
||||||
|
inFlightAgentCount: 0,
|
||||||
|
totalTasksCompleted: 4,
|
||||||
|
totalTasksFailed: 0,
|
||||||
|
});
|
||||||
|
mockTaskStoreListTasks.mockResolvedValue([
|
||||||
|
{ id: "FN-001", column: "in-progress" },
|
||||||
|
{ id: "FN-002", column: "in-progress" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { runProjectList } = await import("../project.js");
|
||||||
|
await runProjectList({ json: true });
|
||||||
|
|
||||||
|
const parsed = JSON.parse(consoleSpy.mock.calls.map((call) => String(call[0])).join(""));
|
||||||
|
expect(parsed[0].health.inFlightAgentCount).toBe(2);
|
||||||
|
expect(parsed[0].health.activeTaskCount).toBe(1);
|
||||||
|
expect(mockGetProjectHealth.mock.results[0]).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runProjectList table prints the live In-Flight column", async () => {
|
||||||
|
mockListProjects.mockResolvedValue([
|
||||||
|
{ id: "proj-1", name: "app-one", path: "/tmp/app-one", status: "active", isolationMode: "in-process" },
|
||||||
|
]);
|
||||||
|
mockGetSettings.mockResolvedValue({});
|
||||||
|
mockGetProjectHealth.mockResolvedValue({
|
||||||
|
projectId: "proj-1",
|
||||||
|
status: "active",
|
||||||
|
activeTaskCount: 1,
|
||||||
|
inFlightAgentCount: 0,
|
||||||
|
totalTasksCompleted: 0,
|
||||||
|
totalTasksFailed: 0,
|
||||||
|
});
|
||||||
|
mockTaskStoreListTasks.mockResolvedValue([
|
||||||
|
{ id: "FN-001", column: "todo" },
|
||||||
|
{ id: "FN-002", column: "in-progress" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { runProjectList } = await import("../project.js");
|
||||||
|
await runProjectList();
|
||||||
|
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||||
|
const projectLine = output.split("\n").find((line) => line.includes("app-one"));
|
||||||
|
expect(output).toContain("In-Flight");
|
||||||
|
expect(projectLine).toContain(" 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runProjectShow reports zero live In-Flight Agents when no tasks are in-progress", async () => {
|
||||||
|
mockGetProject.mockResolvedValue({
|
||||||
|
id: "proj-1",
|
||||||
|
name: "demo",
|
||||||
|
path: "/tmp/demo",
|
||||||
|
status: "active",
|
||||||
|
isolationMode: "in-process",
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
mockGetSettings.mockResolvedValue({});
|
||||||
|
mockGetProjectHealth.mockResolvedValue({
|
||||||
|
projectId: "proj-1",
|
||||||
|
status: "active",
|
||||||
|
activeTaskCount: 2,
|
||||||
|
inFlightAgentCount: 9,
|
||||||
|
totalTasksCompleted: 0,
|
||||||
|
totalTasksFailed: 0,
|
||||||
|
});
|
||||||
|
mockTaskStoreListTasks.mockResolvedValue([
|
||||||
|
{ id: "FN-001", column: "todo" },
|
||||||
|
{ id: "FN-002", column: "done" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { runProjectShow } = await import("../project.js");
|
||||||
|
await runProjectShow("proj-1");
|
||||||
|
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||||
|
expect(output).toContain("In-Flight Agents: 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runProjectShow renders live In-Flight Agents even without a central health row", async () => {
|
||||||
|
mockGetProject.mockResolvedValue({
|
||||||
|
id: "proj-1",
|
||||||
|
name: "demo",
|
||||||
|
path: "/tmp/demo",
|
||||||
|
status: "active",
|
||||||
|
isolationMode: "in-process",
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
mockGetSettings.mockResolvedValue({});
|
||||||
|
mockGetProjectHealth.mockResolvedValue(undefined);
|
||||||
|
mockTaskStoreListTasks.mockResolvedValue([{ id: "FN-001", column: "in-progress" }]);
|
||||||
|
|
||||||
|
const { runProjectShow } = await import("../project.js");
|
||||||
|
await runProjectShow("proj-1");
|
||||||
|
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||||
|
expect(output).toContain("Health:");
|
||||||
|
expect(output).toContain("In-Flight Agents: 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runProjectShow falls back to zero In-Flight Agents when the task store is unreadable", async () => {
|
||||||
|
mockGetProject.mockResolvedValue({
|
||||||
|
id: "proj-1",
|
||||||
|
name: "demo",
|
||||||
|
path: "/tmp/demo",
|
||||||
|
status: "active",
|
||||||
|
isolationMode: "in-process",
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
mockGetSettings.mockResolvedValue({});
|
||||||
|
mockGetProjectHealth.mockResolvedValue({
|
||||||
|
projectId: "proj-1",
|
||||||
|
status: "active",
|
||||||
|
activeTaskCount: 2,
|
||||||
|
inFlightAgentCount: 3,
|
||||||
|
totalTasksCompleted: 0,
|
||||||
|
totalTasksFailed: 0,
|
||||||
|
});
|
||||||
|
mockTaskStoreListTasks.mockRejectedValue(new Error("cannot read tasks"));
|
||||||
|
|
||||||
|
const { runProjectShow } = await import("../project.js");
|
||||||
|
await expect(runProjectShow("proj-1")).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||||
|
expect(output).toContain("In-Flight Agents: 0");
|
||||||
|
});
|
||||||
|
|
||||||
it("validation exits on invalid project name for runProjectAdd", async () => {
|
it("validation exits on invalid project name for runProjectAdd", async () => {
|
||||||
const { runProjectAdd } = await import("../project.js");
|
const { runProjectAdd } = await import("../project.js");
|
||||||
await expect(runProjectAdd("bad name", "/tmp")).rejects.toThrow("process.exit:1");
|
await expect(runProjectAdd("bad name", "/tmp")).rejects.toThrow("process.exit:1");
|
||||||
|
|||||||
@@ -143,6 +143,27 @@ async function getProjectHealth(central: CentralCore, projectId: string): Promis
|
|||||||
return central.getProjectHealth(projectId);
|
return central.getProjectHealth(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLiveInFlightAgentCount(taskCounts: Record<string, number>): number {
|
||||||
|
/*
|
||||||
|
* FNXC:CLIProjectHealth 2026-06-26-18:25:
|
||||||
|
* FN-7081 confirmed central projectHealth.inFlightAgentCount is slot/health bookkeeping that can be stale or zero in the default in-process runtime.
|
||||||
|
* User-visible In-Flight Agents must mirror FN-7080's dashboard read route by deriving the live count from tasks currently in the in-progress column without mutating persisted health rows.
|
||||||
|
*/
|
||||||
|
return taskCounts["in-progress"] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDisplayHealth(
|
||||||
|
taskCounts: Record<string, number>,
|
||||||
|
health?: ProjectHealth
|
||||||
|
): NonNullable<ProjectInfoData["health"]> {
|
||||||
|
return {
|
||||||
|
activeTaskCount: health?.activeTaskCount ?? 0,
|
||||||
|
inFlightAgentCount: getLiveInFlightAgentCount(taskCounts),
|
||||||
|
totalTasksCompleted: health?.totalTasksCompleted ?? 0,
|
||||||
|
totalTasksFailed: health?.totalTasksFailed ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all registered projects.
|
* List all registered projects.
|
||||||
*
|
*
|
||||||
@@ -174,6 +195,8 @@ export async function runProjectList(options: ProjectListOptions = {}): Promise<
|
|||||||
getProjectHealth(central, project.id),
|
getProjectHealth(central, project.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const displayHealth = buildDisplayHealth(taskCounts, health);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
@@ -183,14 +206,7 @@ export async function runProjectList(options: ProjectListOptions = {}): Promise<
|
|||||||
createdAt: project.createdAt,
|
createdAt: project.createdAt,
|
||||||
updatedAt: project.updatedAt,
|
updatedAt: project.updatedAt,
|
||||||
lastActivityAt: health?.lastActivityAt ?? project.lastActivityAt,
|
lastActivityAt: health?.lastActivityAt ?? project.lastActivityAt,
|
||||||
health: health
|
health: displayHealth,
|
||||||
? {
|
|
||||||
activeTaskCount: health.activeTaskCount,
|
|
||||||
inFlightAgentCount: health.inFlightAgentCount,
|
|
||||||
totalTasksCompleted: health.totalTasksCompleted,
|
|
||||||
totalTasksFailed: health.totalTasksFailed,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
taskCounts,
|
taskCounts,
|
||||||
defaultProject: defaultProject?.id === project.id,
|
defaultProject: defaultProject?.id === project.id,
|
||||||
};
|
};
|
||||||
@@ -208,8 +224,8 @@ export async function runProjectList(options: ProjectListOptions = {}): Promise<
|
|||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
console.log(" Name Status Isolation Tasks Last Activity");
|
console.log(" Name Status Isolation Tasks In-Flight Last Activity");
|
||||||
console.log(" " + "─".repeat(72));
|
console.log(" " + "─".repeat(83));
|
||||||
|
|
||||||
for (const project of projectData) {
|
for (const project of projectData) {
|
||||||
const totalTasks = Object.values(project.taskCounts).reduce((a, b) => a + b, 0);
|
const totalTasks = Object.values(project.taskCounts).reduce((a, b) => a + b, 0);
|
||||||
@@ -220,9 +236,10 @@ export async function runProjectList(options: ProjectListOptions = {}): Promise<
|
|||||||
const status = `${statusDot} ${project.status}`.padEnd(12);
|
const status = `${statusDot} ${project.status}`.padEnd(12);
|
||||||
const isolation = project.isolationMode.padEnd(12);
|
const isolation = project.isolationMode.padEnd(12);
|
||||||
const tasks = String(totalTasks).padStart(5);
|
const tasks = String(totalTasks).padStart(5);
|
||||||
|
const inFlightAgents = String(project.health?.inFlightAgentCount ?? 0).padStart(9);
|
||||||
const lastActivity = formatLastActivity(project.lastActivityAt);
|
const lastActivity = formatLastActivity(project.lastActivityAt);
|
||||||
|
|
||||||
console.log(` ${defaultMarker}${name} ${status} ${isolation} ${tasks} ${lastActivity}`);
|
console.log(` ${defaultMarker}${name} ${status} ${isolation} ${tasks} ${inFlightAgents} ${lastActivity}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
@@ -520,16 +537,17 @@ export async function runProjectShow(name?: string): Promise<void> {
|
|||||||
console.log(` Created: ${project.createdAt ?? "unknown"}`);
|
console.log(` Created: ${project.createdAt ?? "unknown"}`);
|
||||||
console.log(` Updated: ${project.updatedAt ?? "unknown"}`);
|
console.log(` Updated: ${project.updatedAt ?? "unknown"}`);
|
||||||
|
|
||||||
if (health) {
|
const displayHealth = buildDisplayHealth(taskCounts, health);
|
||||||
console.log();
|
const lastHealthActivityAt = health?.lastActivityAt ?? project.lastActivityAt;
|
||||||
console.log(` Health:`);
|
|
||||||
console.log(` Active Tasks: ${health.activeTaskCount}`);
|
console.log();
|
||||||
console.log(` In-Flight Agents: ${health.inFlightAgentCount}`);
|
console.log(` Health:`);
|
||||||
console.log(` Completed: ${health.totalTasksCompleted}`);
|
console.log(` Active Tasks: ${displayHealth.activeTaskCount}`);
|
||||||
console.log(` Failed: ${health.totalTasksFailed}`);
|
console.log(` In-Flight Agents: ${displayHealth.inFlightAgentCount}`);
|
||||||
if (health.lastActivityAt) {
|
console.log(` Completed: ${displayHealth.totalTasksCompleted}`);
|
||||||
console.log(` Last Activity: ${formatLastActivity(health.lastActivityAt)}`);
|
console.log(` Failed: ${displayHealth.totalTasksFailed}`);
|
||||||
}
|
if (lastHealthActivityAt) {
|
||||||
|
console.log(` Last Activity: ${formatLastActivity(lastHealthActivityAt)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|||||||
@@ -826,6 +826,10 @@ export async function unregisterProject(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get detailed project info including runtime metrics and task counts.
|
* Get detailed project info including runtime metrics and task counts.
|
||||||
|
*
|
||||||
|
* FNXC:CLIProjectHealth 2026-06-26-18:31:
|
||||||
|
* This resolver returns raw central health for metadata compatibility, so `health.inFlightAgentCount` remains persisted bookkeeping.
|
||||||
|
* Callers that display live running-agent counts must derive them from `taskCounts["in-progress"]` instead of rendering the raw health field.
|
||||||
*/
|
*/
|
||||||
export async function getProjectInfo(name?: string): Promise<{
|
export async function getProjectInfo(name?: string): Promise<{
|
||||||
project: ResolvedProject;
|
project: ResolvedProject;
|
||||||
|
|||||||
@@ -5728,7 +5728,11 @@ export interface ProjectHealth {
|
|||||||
status: ProjectStatus;
|
status: ProjectStatus;
|
||||||
/** Number of tasks currently active */
|
/** Number of tasks currently active */
|
||||||
activeTaskCount: number;
|
activeTaskCount: number;
|
||||||
/** Number of agents currently running */
|
/**
|
||||||
|
* FNXC:Concurrency 2026-06-26-18:34:
|
||||||
|
* Persisted project-health bookkeeping refreshed only by health polling / slot accounting paths; it is not a live read-layer running-agent count.
|
||||||
|
* Consumers that need current running agents must derive from tasks where `column === "in-progress"` (FN-7080/FN-7081) and leave this stored value untouched.
|
||||||
|
*/
|
||||||
inFlightAgentCount: number;
|
inFlightAgentCount: number;
|
||||||
/** ISO-8601 timestamp of last activity */
|
/** ISO-8601 timestamp of last activity */
|
||||||
lastActivityAt?: string;
|
lastActivityAt?: string;
|
||||||
@@ -5772,7 +5776,11 @@ export interface CentralActivityLogEntry {
|
|||||||
export interface GlobalConcurrencyState {
|
export interface GlobalConcurrencyState {
|
||||||
/** System-wide concurrent agent limit (default: 4) */
|
/** System-wide concurrent agent limit (default: 4) */
|
||||||
globalMaxConcurrent: number;
|
globalMaxConcurrent: number;
|
||||||
/** Active agents across all projects */
|
/**
|
||||||
|
* FNXC:Concurrency 2026-06-26-18:34:
|
||||||
|
* Persisted global slot bookkeeping maintained by acquire/release flows; it is not a live aggregate of project task stores.
|
||||||
|
* Read surfaces that need current running-agent totals should aggregate live `column === "in-progress"` task counts while preserving slot limiter semantics and DB column names.
|
||||||
|
*/
|
||||||
currentlyActive: number;
|
currentlyActive: number;
|
||||||
/** Tasks waiting for concurrency slots */
|
/** Tasks waiting for concurrency slots */
|
||||||
queuedCount: number;
|
queuedCount: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user