fix(triage): enforce per-project maxConcurrent limit on triage

The triage processor was only checking the global semaphore, not the
per-project maxConcurrent setting. Now it counts all active agents
(in-progress + specifying) and respects Math.min(perProjectAvailable,
semaphoreAvailable) before starting new triage tasks. This matches the
scheduler's concurrency enforcement and prevents triage from consuming
all available slots on startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-13 15:18:32 -07:00
parent ae91e8cfd0
commit bbc17b460e
4 changed files with 77 additions and 12 deletions

View File

@@ -464,9 +464,10 @@ export class TriageProcessor {
}
this.wasEnginePaused = false;
const tasks = await this.store.listTasks({ slim: true, column: "triage" });
// Fetch all tasks (not just triage) to count active agents across columns.
const allTasks = await this.store.listTasks({ slim: true, includeArchived: false });
const now = Date.now();
const triageTasks = tasks.filter(
const triageTasks = allTasks.filter(
(t) => t.column === "triage" && !this.processing.has(t.id) && !t.paused
// Skip tasks awaiting manual plan approval — they should not be auto-discovered
&& t.status !== "awaiting-approval"
@@ -474,13 +475,26 @@ export class TriageProcessor {
&& !(t.nextRecoveryAt && new Date(t.nextRecoveryAt).getTime() > now),
);
// Respect the global concurrency limit — only kick off as many triage
// tasks as the semaphore has available slots. Without this gate, all
// eligible tasks queue on the semaphore simultaneously, which is
// wasteful and confusing in the UI.
const maxToStart = this.options.semaphore
// Respect both per-project maxConcurrent and the global semaphore.
// Count all active agent slots: in-progress tasks + already-specifying tasks.
const maxConcurrent = settings.maxConcurrent ?? 2;
const inProgress = allTasks.filter((t) => t.column === "in-progress").length;
const specifying = allTasks.filter(
(t) => t.column === "triage" && t.status === "specifying" && !t.paused,
).length;
const activeAgents = inProgress + specifying;
const perProjectAvailable = Math.max(0, maxConcurrent - activeAgents);
const semaphoreAvailable = this.options.semaphore
? Math.max(0, this.options.semaphore.availableCount)
: triageTasks.length;
: Infinity;
const maxToStart = Math.min(perProjectAvailable, semaphoreAvailable);
if (maxToStart <= 0 && triageTasks.length > 0) {
triageLog.log(
`Triage throttled: ${activeAgents} active agents (${inProgress} executing, ${specifying} specifying), limit ${maxConcurrent}`,
);
}
for (let i = 0; i < Math.min(triageTasks.length, maxToStart); i++) {
void this.specifyTask(triageTasks[i]);