feat(FN-4538): complete Step 2 — preserve overlap blockers in scheduler

Fusion-Task-Id: FN-4538
Fusion-Task-Lineage: 332b4c78-b67f-45d0-9181-0bb539b662c1
This commit is contained in:
Fusion
2026-05-14 21:49:08 -07:00
committed by gsxdsm
parent 1e9de7f2e8
commit 083f53a523
2 changed files with 29 additions and 14 deletions

View File

@@ -1029,7 +1029,7 @@ describe("Scheduler", () => {
// Dependency-blocked urgent task should be queued, not started.
expect(updateTask).toHaveBeenCalledWith("FN-100", { status: "queued", blockedBy: "FN-900" });
// Overlap-blocked urgent task should be queued with blocker id.
expect(updateTask).toHaveBeenCalledWith("FN-103", { status: "queued", blockedBy: "FN-001" });
expect(updateTask).toHaveBeenCalledWith("FN-103", { status: "queued", blockedBy: "FN-001", overlapBlockedBy: "FN-001" });
// Paused and recovery-gated urgent tasks never enter scheduling.
expect(moveTask).not.toHaveBeenCalledWith("FN-101", "in-progress");
expect(moveTask).not.toHaveBeenCalledWith("FN-102", "in-progress");
@@ -1262,7 +1262,7 @@ describe("Scheduler", () => {
(scheduler as any).running = true;
await scheduler.schedule();
expect(updateTask).toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001" });
expect(updateTask).toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001", overlapBlockedBy: "FN-001" });
expect(moveTask).not.toHaveBeenCalledWith("FN-002", "in-progress");
});
@@ -1300,7 +1300,7 @@ describe("Scheduler", () => {
(scheduler as any).running = true;
await scheduler.schedule();
expect(updateTask).toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001" });
expect(updateTask).toHaveBeenCalledWith("FN-002", { status: "queued", blockedBy: "FN-001", overlapBlockedBy: "FN-001" });
expect(moveTask).not.toHaveBeenCalledWith("FN-002", "in-progress");
});
});
@@ -1379,7 +1379,7 @@ describe("Scheduler", () => {
(scheduler as any).running = true;
await scheduler.schedule();
expect(updateTask).toHaveBeenCalledWith("FN-T", { status: "queued", blockedBy: "FN-B" });
expect(updateTask).toHaveBeenCalledWith("FN-T", { status: "queued", blockedBy: "FN-B", overlapBlockedBy: "FN-B" });
});
it("does not stamp blockedBy for todos without overlap, including empty scopes", async () => {

View File

@@ -894,9 +894,10 @@ export class Scheduler {
);
if (taskScope.length > 0) {
const activeScopeEntries = Array.from(activeScopes.entries()).sort(([aId], [bId]) => aId.localeCompare(bId));
const currentBlockerScope = task.blockedBy ? activeScopes.get(task.blockedBy) : undefined;
const overlapBlockerId = task.overlapBlockedBy || task.blockedBy;
const currentBlockerScope = overlapBlockerId ? activeScopes.get(overlapBlockerId) : undefined;
const hasValidCurrentBlocker =
Boolean(task.blockedBy)
Boolean(overlapBlockerId)
&& Boolean(currentBlockerScope)
&& this.pathsOverlap(taskScope, currentBlockerScope!);
@@ -907,22 +908,36 @@ export class Scheduler {
* - idempotent writes only: update DB only when blockedBy/status must change
*/
const overlappingTaskId = hasValidCurrentBlocker
? task.blockedBy
? overlapBlockerId
: activeScopeEntries.find(([, ipScope]) => this.pathsOverlap(taskScope, ipScope))?.[0] ?? null;
if (overlappingTaskId) {
// Keep blockedBy tied to explicit unresolved dependencies when a task has
// dependency edges; avoid repointing dependency-unblocked tasks to unrelated
// overlap ids (FN-3924). For dependency-free tasks, blockedBy may reference
// the active overlap blocker.
const targetBlockedBy = task.dependencies.length > 0 ? null : overlappingTaskId;
if (task.status !== "queued" || task.blockedBy !== targetBlockedBy) {
await this.store.updateTask(task.id, { status: "queued", blockedBy: targetBlockedBy });
const unresolvedDeps = task.dependencies.filter((depId) => {
const dep = tasks.find((t) => t.id === depId);
return dep && dep.column !== "done" && dep.column !== "in-review" && dep.column !== "archived";
});
const targetBlockedBy = task.dependencies.length > 0
? (unresolvedDeps[0] ?? null)
: overlappingTaskId;
if (
task.status !== "queued"
|| task.blockedBy !== targetBlockedBy
|| task.overlapBlockedBy !== overlappingTaskId
) {
await this.store.updateTask(task.id, {
status: "queued",
blockedBy: targetBlockedBy,
overlapBlockedBy: overlappingTaskId,
});
}
await this.rollbackRunningAgentsForQueuedTodoTask(task.id);
await this.logDispatchQueuedReason(task.id, `queued — file scope overlap with ${overlappingTaskId}`);
continue;
}
if (task.overlapBlockedBy) {
await this.store.updateTask(task.id, { overlapBlockedBy: null });
}
}
}