feat(HAI-016): complete Step 5 — wire semaphore into dashboard engine components

This commit is contained in:
Dustin Byrne
2026-03-25 21:25:07 -04:00
parent 6a45531a79
commit 0f80d4f2b2
3 changed files with 26 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
import { resolveDependencyOrder, type TaskStore, type Task } from "@hai/core";
import type { AgentSemaphore } from "./concurrency.js";
export interface SchedulerOptions {
/** Max concurrent in-progress tasks. Default: 2 */
@@ -7,6 +8,13 @@ export interface SchedulerOptions {
maxWorktrees?: number;
/** Milliseconds between scheduling polls. Default: 15000 */
pollIntervalMs?: number;
/**
* Shared concurrency semaphore. When provided, the scheduler uses
* `semaphore.availableCount` to avoid scheduling more tasks than the
* global concurrency limit allows (accounting for triage and merge
* agents that also hold slots).
*/
semaphore?: AgentSemaphore;
/** Called when scheduler starts a task */
onSchedule?: (task: Task) => void;
/** Called when a task is blocked by deps */
@@ -106,9 +114,19 @@ export class Scheduler {
}
const inProgress = tasks.filter((t) => t.column === "in-progress");
// When a semaphore is provided, factor in its available slots so we
// don't schedule more tasks than the global limit allows. Triage and
// merge agents also hold semaphore slots, so availableCount may be
// lower than what maxConcurrent - inProgress.length would suggest.
const semaphoreAvailable = this.options.semaphore
? this.options.semaphore.availableCount
: Infinity;
const available = Math.min(
maxConcurrent - inProgress.length,
maxWorktrees - activeWorktrees,
semaphoreAvailable,
);
if (available <= 0) return;