fix(FN-6043): recover stuck task processing

Fusion-Task-Id: FN-6043
This commit is contained in:
gsxdsm
2026-06-08 18:08:42 -07:00
parent ed0dc4a7b9
commit aa8bd3dc92
21 changed files with 643 additions and 164 deletions

View File

@@ -634,6 +634,7 @@ export class TriageProcessor {
private processingSince = new Map<string, number>();
private wasGlobalPaused = false;
private wasEnginePaused = false;
private idleSemaphoreLeakCandidateSince: number | null = null;
/** Active agent sessions per task, used to terminate on pause. */
private activeSessions = new Map<string, { dispose: () => void }>();
/**
@@ -997,6 +998,31 @@ export class TriageProcessor {
// 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();
if (this.options.semaphore) {
const persistedActive = allTasks.filter((task) => (
task.column === "in-progress"
|| (task.column === "triage" && task.status === "planning" && !task.paused)
|| (task.column === "in-review" && ["merging", "reviewing", "fixing"].includes(String(task.status ?? "")))
)).length;
if (persistedActive === 0 && this.options.semaphore.activeCount > 0 && this.processing.size === 0) {
if (this.idleSemaphoreLeakCandidateSince === null) {
this.idleSemaphoreLeakCandidateSince = now;
} else if (now - this.idleSemaphoreLeakCandidateSince >= 5_000) {
const result = this.options.semaphore.reconcileActiveCount(0);
if (result.changed) {
planLog.warn(
`triage: recovered stale semaphore active count ${result.before} -> ${result.after} ` +
"(no persisted in-progress/planning/review agent work)",
);
}
this.idleSemaphoreLeakCandidateSince = null;
}
} else {
this.idleSemaphoreLeakCandidateSince = null;
}
}
const eligibleTriageTasks = 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
@@ -1043,8 +1069,17 @@ export class TriageProcessor {
const maxToStart = Math.min(perProjectAvailable, semaphoreAvailable);
if (maxToStart <= 0 && triageTasks.length > 0) {
const semaphoreSnapshot = this.options.semaphore?.snapshot();
const semaphoreDetail = semaphoreSnapshot
? `, semaphore active=${semaphoreSnapshot.activeCount}/${semaphoreSnapshot.limit}, available=${semaphoreSnapshot.availableCount}, waiting=${semaphoreSnapshot.waitingCount}`
: ", semaphore unavailable";
const processingIds = [...this.processing].slice(0, 5);
const eligibleIds = triageTasks.slice(0, 5).map((t) => t.id);
const blockedBy = perProjectAvailable <= 0 ? "triage concurrency" : "global semaphore";
planLog.log(
`Plan throttled: ${activeAgents} planning agents, limit ${maxTriageConcurrent}`,
`Plan throttled by ${blockedBy}: eligible=${triageTasks.length} [${eligibleIds.join(", ")}], ` +
`planning=${activeAgents}/${maxTriageConcurrent}, processing=${this.processing.size}` +
`${processingIds.length > 0 ? ` [${processingIds.join(", ")}]` : ""}${semaphoreDetail}`,
);
}