From a98713d01da2cb1de55db617809f4e8fdba863cf Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 8 Jun 2026 22:47:51 -0700 Subject: [PATCH] fix(FN-6035): cap scheduler dispatch by global slots --- .../engine/src/__tests__/scheduler.test.ts | 28 +++++++++++++++++++ packages/engine/src/scheduler.ts | 5 +++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/__tests__/scheduler.test.ts b/packages/engine/src/__tests__/scheduler.test.ts index 0f89d9f09d..8c6004a8d7 100644 --- a/packages/engine/src/__tests__/scheduler.test.ts +++ b/packages/engine/src/__tests__/scheduler.test.ts @@ -1411,6 +1411,34 @@ describe("Scheduler", () => { expect(store.moveTask).not.toHaveBeenCalled(); }); + it("caps in-progress dispatch by the global semaphore limit even before executors acquire slots", async () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFile).mockResolvedValue("# Task\nDo something"); + + const semaphore = new AgentSemaphore(3); + const tasks = [ + createMockTask({ id: "FN-001", column: "in-progress" }), + createMockTask({ id: "FN-002", column: "in-progress" }), + createMockTask({ id: "FN-003", column: "in-progress" }), + createMockTask({ id: "FN-004", column: "todo", dependencies: [] }), + createMockTask({ id: "FN-005", column: "todo", dependencies: [] }), + ]; + + const store = createMockStore({ + listTasks: vi.fn().mockResolvedValue(tasks), + getTask: vi.fn(async (taskId: string) => tasks.find((task) => task.id === taskId) ?? null), + getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 5, maxWorktrees: 10 }), + updateTask: vi.fn().mockResolvedValue(undefined), + moveTask: vi.fn().mockResolvedValue(undefined), + }); + + const scheduler = new Scheduler(store, { semaphore }); + scheduler.start(); + await scheduler.schedule(); + + expect(store.moveTask).not.toHaveBeenCalled(); + }); + it("respects maxWorktrees limit", async () => { const tasks = [ createMockTask({ id: "FN-001", column: "in-progress" }), diff --git a/packages/engine/src/scheduler.ts b/packages/engine/src/scheduler.ts index c099ccfcdb..7db22e4db9 100644 --- a/packages/engine/src/scheduler.ts +++ b/packages/engine/src/scheduler.ts @@ -1277,7 +1277,10 @@ export class Scheduler { // When a semaphore is provided, factor in its available slots so we // don't schedule more tasks than the global limit allows. const semaphoreAvailable = this.options.semaphore - ? this.options.semaphore.availableCount + ? Math.min( + this.options.semaphore.availableCount, + this.options.semaphore.limit - agentSlots, + ) : Infinity; const available = Math.min(