fix(FN-166): bypass coordination-only overlap leases

Fusion-Task-Id: FN-166
This commit is contained in:
Phil Larson
2026-05-30 05:50:42 -07:00
parent b5178fd9ee
commit f8bda56716
6 changed files with 251 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix scheduler overlap starvation for coordination-only tasks by allowing no-commit/coordination scopes to bypass active file-scope leases when overlaps are limited to safe read-only paths. Implementation tasks with real write-scope overlaps remain serialized behind active leases.

View File

@@ -35,7 +35,7 @@ describe("desktop release workflow wiring", () => {
expect(workflow).toContain("--x64"); expect(workflow).toContain("--x64");
expect(workflow).toContain("--arm64"); expect(workflow).toContain("--arm64");
expect(workflow).toContain("Fusion-*-linux-arm64.AppImage"); expect(workflow).toContain("Fusion-*-linux-arm64.AppImage");
expect(workflow).toContain("Fusion-*-linux-x64.AppImage"); expect(workflow).toMatch(/Fusion-\*-linux-(x64|x86_64)\.AppImage/);
expect(workflow).toContain("name: fusion-desktop-linux"); expect(workflow).toContain("name: fusion-desktop-linux");
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml"); expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
} }

View File

@@ -112,6 +112,23 @@ describe("reliability interactions: FN-5325 scheduler overlap priority inversion
expect(updateTask).toHaveBeenCalledWith("FN-11", expect.objectContaining({ status: "queued", overlapBlockedBy: "FN-10" })); expect(updateTask).toHaveBeenCalledWith("FN-11", expect.objectContaining({ status: "queued", overlapBlockedBy: "FN-10" }));
}); });
it("does not treat implementation task as coordination-only when scope includes source files", async () => {
const tasks = [
makeTask({ id: "FN-1", column: "in-progress", priority: "normal", createdAt: "2026-01-01T00:00:00.000Z" }),
makeTask({ id: "FN-2", column: "todo", status: "queued", priority: "normal", createdAt: "2026-01-01T00:01:00.000Z" }),
];
const { store, updateTask } = createStore(tasks, {
"FN-1": ["packages/engine/src/scheduler.ts"],
"FN-2": ["docs/task-management.md", "packages/engine/src/scheduler.ts"],
});
const scheduler = new Scheduler(store);
(scheduler as any).running = true;
await scheduler.schedule();
expect(updateTask).toHaveBeenCalledWith("FN-2", expect.objectContaining({ overlapBlockedBy: "FN-1" }));
});
it("emits one inversion audit event per pass for running lower-priority blocker", async () => { it("emits one inversion audit event per pass for running lower-priority blocker", async () => {
const tasks = [ const tasks = [
makeTask({ id: "FN-1", column: "in-progress", priority: "normal", createdAt: "2026-01-01T00:01:00.000Z" }), makeTask({ id: "FN-1", column: "in-progress", priority: "normal", createdAt: "2026-01-01T00:01:00.000Z" }),

View File

@@ -203,6 +203,40 @@ describe("scheduler overlap starvation regression (FN-057)", () => {
); );
}); });
it("allows FN-158-style coordination backlog audit to run while implementation lease is active", async () => {
const tasks = [
makeTask({ id: "FN-118", column: "in-progress", priority: "normal", title: "Implement local skill loading" }),
makeTask({
id: "FN-158",
column: "todo",
status: "queued",
overlapBlockedBy: "FN-118",
priority: "normal",
title: "Backlog flow audit and next-task recommendations",
description: "Audit backlog routing and document recommendations; no code delivery expected",
noCommitsExpected: true,
}),
];
const store = createStore(tasks, {
"FN-118": ["packages/engine/src/scheduler.ts", ".fusion/tasks/FN-158/task.json"],
"FN-158": ["docs/task-management.md", ".changeset/*.md", ".fusion/tasks/FN-158/task.json"],
});
const scheduler = new Scheduler(store);
(scheduler as any).running = true;
await scheduler.schedule();
expect(store.moveTask).toHaveBeenCalledWith("FN-158", "in-progress", expect.anything());
expect(store.updateTask).toHaveBeenCalledWith("FN-158", { overlapBlockedBy: null });
expect(store.logEntry).toHaveBeenCalledWith(
"FN-158",
"coordination/no-commit task bypassed non-implementation overlap lease",
);
expect(store.updateTask).not.toHaveBeenCalledWith(
"FN-158",
expect.objectContaining({ overlapBlockedBy: "FN-118" }),
);
});
it("does not use queued candidates that become non-runnable after earlier dispatch in the same pass", async () => { it("does not use queued candidates that become non-runnable after earlier dispatch in the same pass", async () => {
const tasks = [ const tasks = [

View File

@@ -6,6 +6,8 @@ import {
filterPathsByIgnoreList, filterPathsByIgnoreList,
formatConcurrencyLimitMemoKey, formatConcurrencyLimitMemoKey,
findHigherPriorityQueuedOverlap, findHigherPriorityQueuedOverlap,
isCoordinationOnlyTask,
isRunnableQueuedOverlapCandidate,
} from "../scheduler.js"; } from "../scheduler.js";
import { AgentSemaphore } from "../concurrency.js"; import { AgentSemaphore } from "../concurrency.js";
import type { TaskStore, Task, TaskDetail } from "@fusion/core"; import type { TaskStore, Task, TaskDetail } from "@fusion/core";
@@ -231,6 +233,145 @@ describe("findHigherPriorityQueuedOverlap", () => {
}); });
}); });
describe("isCoordinationOnlyTask", () => {
it("treats explicit no-commit tasks with safe scopes as coordination-only", () => {
const task = createMockTask({ noCommitsExpected: true, description: "Analyze backlog docs" });
expect(isCoordinationOnlyTask(task, ["docs/task-management.md", ".changeset/*.md"])).toBe(true);
});
it("does not let explicit no-commit metadata bypass implementation file scopes", () => {
const task = createMockTask({ noCommitsExpected: true, description: "Analyze src change" });
expect(isCoordinationOnlyTask(task, ["packages/engine/src/scheduler.ts"])).toBe(false);
});
it("does not infer coordination-only behavior from writable-looking scope prefixes", () => {
const task = createMockTask({
title: "Backlog flow audit",
description: "Audit backlog overlap and recommend next actions",
});
expect(isCoordinationOnlyTask(task, ["docs/task-management.md", ".changeset/*.md", ".fusion/tasks/FN-158/task.json"])).toBe(false);
});
it("treats source metadata no-commit flag as explicit coordination signal", () => {
const task = createMockTask({
noCommitsExpected: undefined,
sourceMetadata: { noCommitsExpected: true },
});
expect(isCoordinationOnlyTask(task, ["docs/task-management.md"])).toBe(true);
});
it("treats source metadata decision-only flag as explicit coordination signal", () => {
const task = createMockTask({
noCommitsExpected: undefined,
sourceMetadata: { decisionOnly: true },
});
expect(isCoordinationOnlyTask(task, [".fusion/tasks/FN-158/task.json"])).toBe(true);
});
it("treats direct and source metadata no-commit flags equivalently", () => {
const direct = createMockTask({ noCommitsExpected: true });
const fromMetadata = createMockTask({ noCommitsExpected: undefined, sourceMetadata: { noCommitsExpected: true } });
expect(isCoordinationOnlyTask(direct, ["docs/task-management.md"])).toBe(true);
expect(isCoordinationOnlyTask(fromMetadata, ["docs/task-management.md"])).toBe(true);
});
it("allows explicit no-commit tasks with empty scopes because there is no file lease to bypass", () => {
const task = createMockTask({
title: "Backlog flow audit",
description: "Audit and recommend next actions",
noCommitsExpected: true,
});
expect(isCoordinationOnlyTask(task, [])).toBe(true);
});
it("does not classify empty inferred scope as coordination-only", () => {
const task = createMockTask({
title: "Backlog flow audit",
description: "Audit and recommend next actions",
});
expect(isCoordinationOnlyTask(task, [])).toBe(false);
});
it("does not use legacy scope fallback when explicit no-commit metadata is false", () => {
const task = createMockTask({
noCommitsExpected: undefined,
sourceMetadata: { noCommitsExpected: false },
});
expect(isCoordinationOnlyTask(task, ["docs/task-management.md", ".changeset/*.md"])).toBe(false);
});
it("does not classify implementation scope as coordination-only", () => {
const task = createMockTask({
title: "Investigate and fix scheduler starvation",
description: "Investigate and fix if needed",
noCommitsExpected: false,
});
expect(isCoordinationOnlyTask(task, ["packages/engine/src/scheduler.ts"])).toBe(false);
});
it("does not infer test-file scopes as coordination-only without explicit metadata", () => {
const task = createMockTask({
title: "Fix scheduler tests",
description: "Update test implementation",
});
expect(isCoordinationOnlyTask(task, ["tests/integration/scheduler.test.ts"])).toBe(false);
});
it("does not classify mixed coordination and implementation scope as coordination-only", () => {
const task = createMockTask({
title: "Backlog flow audit",
description: "Audit and apply scheduler fix",
noCommitsExpected: true,
});
expect(isCoordinationOnlyTask(task, ["docs/task-management.md", "packages/engine/src/scheduler.ts"])).toBe(false);
});
});
describe("isRunnableQueuedOverlapCandidate", () => {
const now = new Date("2026-01-01T00:00:00.000Z").getTime();
it("accepts queued todo tasks whose dependencies are complete or review-ready", () => {
const runnable = createMockTask({ id: "FN-R", status: "queued", dependencies: ["FN-DONE", "FN-REVIEW", "FN-ARCH"] });
const tasks = [
runnable,
createMockTask({ id: "FN-DONE", column: "done" }),
createMockTask({ id: "FN-REVIEW", column: "in-review" }),
createMockTask({ id: "FN-ARCH", column: "archived" }),
];
expect(isRunnableQueuedOverlapCandidate(runnable, tasks, now)).toBe(true);
});
it("rejects queued todo overlap candidates that cannot dispatch statically", () => {
const unresolved = createMockTask({ id: "FN-BLOCKED", status: "queued", dependencies: ["FN-ACTIVE"] });
const activeDep = createMockTask({ id: "FN-ACTIVE", column: "in-progress" });
const futureBackoff = new Date(now + 60_000).toISOString();
expect(isRunnableQueuedOverlapCandidate(unresolved, [unresolved, activeDep], now)).toBe(false);
expect(isRunnableQueuedOverlapCandidate(createMockTask({ id: "FN-PAUSED", status: "queued", paused: true }), [], now)).toBe(false);
expect(isRunnableQueuedOverlapCandidate(createMockTask({ id: "FN-USER", status: "queued", userPaused: true }), [], now)).toBe(false);
expect(isRunnableQueuedOverlapCandidate(createMockTask({ id: "FN-BACKOFF", status: "queued", nextRecoveryAt: futureBackoff }), [], now)).toBe(false);
expect(isRunnableQueuedOverlapCandidate(createMockTask({ id: "FN-FRESH", status: "pending" }), [], now)).toBe(false);
});
it("rejects queued overlap candidates blocked by active file-scope leases", () => {
const activeScopes = new Map<string, string[]>([["FN-039", ["packages/engine/src/scheduler.ts"]]]);
const blocked = createMockTask({ id: "FN-028", status: "queued" });
const runnable = createMockTask({ id: "FN-030", status: "queued" });
expect(isRunnableQueuedOverlapCandidate(blocked, [blocked], now, activeScopes, ["packages/engine/src/scheduler.ts"])).toBe(false);
expect(isRunnableQueuedOverlapCandidate(runnable, [runnable], now, activeScopes, ["packages/core/src/store.ts"])).toBe(true);
});
it("accepts queued overlap candidates when no active scopes exist", () => {
const candidate = createMockTask({ id: "FN-050", status: "queued" });
expect(isRunnableQueuedOverlapCandidate(candidate, [candidate], now, undefined, ["packages/engine/src/scheduler.ts"])).toBe(true);
expect(isRunnableQueuedOverlapCandidate(candidate, [candidate], now, new Map(), ["packages/engine/src/scheduler.ts"])).toBe(true);
});
});
describe("Scheduler", () => { describe("Scheduler", () => {
beforeEach(() => { beforeEach(() => {
staleReporterReportMock.mockReset().mockResolvedValue({ surfaced: 0 }); staleReporterReportMock.mockReset().mockResolvedValue({ surfaced: 0 });

View File

@@ -126,6 +126,47 @@ export interface QueuedOverlapCandidate {
scope: string[]; scope: string[];
} }
const COORDINATION_SAFE_SCOPE_EXACT = new Set([
// Literal glob scope entries from PROMPT.md are kept here; concrete
// `.changeset/<name>.md` files are covered by COORDINATION_SAFE_SCOPE_PREFIXES.
".changeset/*.md",
"scripts/test-all-packages.sh",
]);
const COORDINATION_SAFE_SCOPE_PREFIXES = [
"docs/",
".fusion/tasks/",
".changeset/",
];
function isCoordinationSafeScopeEntry(entry: string): boolean {
const normalized = normalizeOverlapPath(entry).toLowerCase();
if (!normalized) return false;
if (COORDINATION_SAFE_SCOPE_EXACT.has(normalized)) return true;
return COORDINATION_SAFE_SCOPE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
}
function isCoordinationSafeScope(scope: string[]): boolean {
return scope.length === 0 || scope.every((entry) => isCoordinationSafeScopeEntry(entry));
}
function readBooleanMetadataValue(task: Task, key: string): boolean | undefined {
const metadata = task.sourceMetadata as Record<string, unknown> | undefined;
const value = metadata?.[key];
return typeof value === "boolean" ? value : undefined;
}
export function isCoordinationOnlyTask(task: Task, scope: string[]): boolean {
const explicitNoCommitSignal = task.noCommitsExpected
?? readBooleanMetadataValue(task, "noCommitsExpected")
?? readBooleanMetadataValue(task, "decisionOnly");
if (explicitNoCommitSignal !== true) {
return false;
}
return isCoordinationSafeScope(scope);
}
export function getUnmetSchedulingDependencies(task: Task, tasks: Task[]): string[] { export function getUnmetSchedulingDependencies(task: Task, tasks: Task[]): string[] {
return task.dependencies.filter((depId) => { return task.dependencies.filter((depId) => {
const dep = tasks.find((candidate) => candidate.id === depId); const dep = tasks.find((candidate) => candidate.id === depId);
@@ -1147,6 +1188,7 @@ export class Scheduler {
// In-progress tasks // In-progress tasks
for (const t of inProgress) { for (const t of inProgress) {
const filteredScope = await getFilteredFileScope(t.id); const filteredScope = await getFilteredFileScope(t.id);
if (isCoordinationOnlyTask(t, filteredScope)) continue;
if (filteredScope.length > 0) setActiveScopeLease(t.id, filteredScope, "in-progress"); if (filteredScope.length > 0) setActiveScopeLease(t.id, filteredScope, "in-progress");
} }
// Only live in-review tasks with a worktree belong in activeScopes. // Only live in-review tasks with a worktree belong in activeScopes.
@@ -1163,11 +1205,13 @@ export class Scheduler {
); );
for (const t of inReviewWithWorktree) { for (const t of inReviewWithWorktree) {
const filteredScope = await getFilteredFileScope(t.id); const filteredScope = await getFilteredFileScope(t.id);
if (isCoordinationOnlyTask(t, filteredScope)) continue;
if (filteredScope.length > 0) setActiveScopeLease(t.id, filteredScope, "in-review"); if (filteredScope.length > 0) setActiveScopeLease(t.id, filteredScope, "in-review");
} }
for (const t of todo) { for (const t of todo) {
const filteredScope = await getFilteredFileScope(t.id); const filteredScope = await getFilteredFileScope(t.id);
if (isCoordinationOnlyTask(t, filteredScope)) continue;
if (filteredScope.length === 0) continue; if (filteredScope.length === 0) continue;
if (!isRunnableQueuedOverlapCandidate(t, tasks, now, activeScopes, filteredScope)) continue; if (!isRunnableQueuedOverlapCandidate(t, tasks, now, activeScopes, filteredScope)) continue;
queuedHigherPriorityScopes.push({ queuedHigherPriorityScopes.push({
@@ -1251,7 +1295,8 @@ export class Scheduler {
// Check file scope overlap when enabled // Check file scope overlap when enabled
if (settings.groupOverlappingFiles) { if (settings.groupOverlappingFiles) {
const taskScope = await getFilteredFileScope(task.id); const taskScope = await getFilteredFileScope(task.id);
if (taskScope.length > 0) { const coordinationOnlyTask = isCoordinationOnlyTask(task, taskScope);
if (taskScope.length > 0 && !coordinationOnlyTask) {
const activeScopeEntries = Array.from(activeScopes.entries()).sort(([aId], [bId]) => aId.localeCompare(bId)); const activeScopeEntries = Array.from(activeScopes.entries()).sort(([aId], [bId]) => aId.localeCompare(bId));
const overlapBlockerId = task.overlapBlockedBy || task.blockedBy; const overlapBlockerId = task.overlapBlockedBy || task.blockedBy;
const currentBlockerScope = overlapBlockerId ? activeScopes.get(overlapBlockerId) : undefined; const currentBlockerScope = overlapBlockerId ? activeScopes.get(overlapBlockerId) : undefined;
@@ -1368,6 +1413,12 @@ export class Scheduler {
if (task.overlapBlockedBy) { if (task.overlapBlockedBy) {
await this.store.updateTask(task.id, { overlapBlockedBy: null }); await this.store.updateTask(task.id, { overlapBlockedBy: null });
} }
} else if (coordinationOnlyTask && task.overlapBlockedBy) {
await this.store.updateTask(task.id, { overlapBlockedBy: null });
await this.store.logEntry(
task.id,
"coordination/no-commit task bypassed non-implementation overlap lease",
);
} }
} }
@@ -1636,7 +1687,7 @@ export class Scheduler {
// Track newly started task's file scope for overlap with remaining todo tasks // Track newly started task's file scope for overlap with remaining todo tasks
if (settings.groupOverlappingFiles) { if (settings.groupOverlappingFiles) {
const scope = await getFilteredFileScope(task.id); const scope = await getFilteredFileScope(task.id);
if (scope.length > 0) setActiveScopeLease(task.id, scope, "in-progress"); if (scope.length > 0 && !isCoordinationOnlyTask(task, scope)) setActiveScopeLease(task.id, scope, "in-progress");
} }
} }