import type { WorkspaceAccess } from "../agents/types"; /** Options for scheduling one queued agent run. */ export interface QueueTaskOptions { workspaceAccess: WorkspaceAccess; } /** Handle returned when scheduling one queued task. */ export interface QueuedTaskHandle { promise: Promise; cancel(): boolean; } interface QueueTask { options: QueueTaskOptions; run: () => Promise; resolve(value: T): void; reject(error: unknown): void; isCancelled: boolean; hasStarted: boolean; } /** Error used when one queued task is cancelled before it starts. */ export class QueuedTaskCancelledError extends Error { constructor() { super("Queued task was cancelled"); this.name = "QueuedTaskCancelledError"; } } /** * Concurrency limiter that allows up to N read runs or exactly one write run. */ export class ConcurrencyQueue { private readonly pendingTasks: Array> = []; private activeTaskCount = 0; private hasActiveWriteTask = false; constructor(private readonly maxConcurrency: number) { if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) { throw new Error("maxConcurrency must be at least 1"); } } /** Schedules one task and returns a promise plus a queued-task cancel handle. */ enqueue(run: () => Promise, options: QueueTaskOptions): QueuedTaskHandle { let queueTask: QueueTask | undefined; const promise = new Promise((resolve, reject) => { queueTask = { options, run, resolve, reject, isCancelled: false, hasStarted: false, }; }); if (!queueTask) { throw new Error("Failed to initialize queue task"); } this.pendingTasks.push(queueTask as QueueTask); this.drain(); return { promise, cancel: () => this.cancelTask(queueTask as QueueTask), }; } private cancelTask(task: QueueTask): boolean { if (task.hasStarted || task.isCancelled) { return false; } task.isCancelled = true; const taskIndex = this.pendingTasks.indexOf(task); if (taskIndex >= 0) { this.pendingTasks.splice(taskIndex, 1); } task.reject(new QueuedTaskCancelledError()); this.drain(); return true; } private drain(): void { while (this.pendingTasks.length > 0) { const nextTask = this.pendingTasks[0]; if (!nextTask) { return; } if (!this.canStartTask(nextTask)) { return; } this.pendingTasks.shift(); if (nextTask.isCancelled) { continue; } nextTask.hasStarted = true; this.activeTaskCount += 1; if (nextTask.options.workspaceAccess === "write") { this.hasActiveWriteTask = true; } void this.runTask(nextTask); } } private canStartTask(task: QueueTask): boolean { if (task.options.workspaceAccess === "write") { return this.activeTaskCount === 0; } return !this.hasActiveWriteTask && this.activeTaskCount < this.maxConcurrency; } private async runTask(task: QueueTask): Promise { try { const result = await task.run(); task.resolve(result); } catch (error) { task.reject(error); } finally { this.activeTaskCount -= 1; if (task.options.workspaceAccess === "write") { this.hasActiveWriteTask = false; } this.drain(); } } }