fix(engine): run merges outside the admission drain — every merge froze planning admission

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 18:13:32 -07:00
parent e51ebff381
commit 00769fad7c
2 changed files with 30 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Planning admission no longer freezes for the duration of every merge.
category: fix
dev: Root cause of the 5-10 min "Queued to plan" stalls — the merge lane ran the entire merge inside projectAdmissionCoordinator's single-flight drain, so triage's poll parked awaiting it (and its re-entrance guard then dropped every tick silently). The lane start now claims and returns; the merge body runs outside the drain. No automated regression test yet — the drain-blocking shape needs a project-engine harness; the triage poll watchdog (e51ebff381) is the backstop meanwhile.

View File

@@ -3881,7 +3881,24 @@ export class ProjectEngine {
}
}
let selected = false;
let value: T | undefined;
/*
FNXC:ConcurrencyAdmission 2026-08-01-01:50 (ROOT CAUSE — triage admission died during every merge):
This lane previously ran `value = await start()` INSIDE its admission `start()` callback —
i.e. the ENTIRE merge (git rebase, verification, landing: minutes, or forever when the
merge wedges) executed inside `admitOldest`'s single-flight drain. The coordinator is a
project-wide singleton and every caller awaits the previous drain, so triage's poll parked
at `await existing` for the whole merge window, its `polling` re-entrance guard stayed
closed, and every 15s tick + task:created wake dropped silently. Observed twice on the
live board as "queued to plan with open capacity" (5m50s behind FN-8627's merge; ~10min
behind FN-8635's adoption-paused landing), each ending in a batch admission the second
the merge finished. With merge pinned at 1, every merge was a planning outage.
The lane start now only CLAIMS the admission and returns; the merge body runs after
`admitOldest` settles, outside the drain. Capacity stays honest: the merge row's own
merging/landing status is what `claimed()` counts, and at-most-once merging is enforced
by the merge lease, not by this drain. The transient admit→status-write gap is the same
one every other lane (triage `void specifyTask`, scheduler `void schedule`) already has.
*/
await projectAdmissionCoordinator.admitOldest({
projectId: cwd,
maxConcurrent: (await store.getSettings()).maxConcurrent ?? 2,
@@ -3895,14 +3912,16 @@ export class ProjectEngine {
createdAt: mergeCandidate?.createdAt,
start: async () => {
selected = true;
value = await start();
return true;
},
}],
});
if (!selected) return undefined;
projectAdmissionCoordinator.releaseReservation(taskId);
return value;
try {
return await start();
} finally {
projectAdmissionCoordinator.releaseReservation(taskId);
}
};
if (mergeStrategy === "pull-request" && this.options.processPullRequestMerge && !routeWorkspaceDirect) {