fix(engine): a Ready card's retained worktree transfers on release instead of blocking it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/worktree-slot-transfer-on-release.md
Normal file
7
.changeset/worktree-slot-transfer-on-release.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Planned tasks release at full concurrency again — a card's retained planning worktree no longer blocks its own release.
|
||||||
|
category: fix
|
||||||
|
dev: Follow-up to the widened maxWorktrees ledger: a Ready card reuses its planning worktree on release, so its held slot transfers instead of double-counting. Observed live as only 2 of 4 slots releasing after unpause.
|
||||||
@@ -2254,6 +2254,16 @@ export class Scheduler {
|
|||||||
&& !isTerminalColumnTask(task)
|
&& !isTerminalColumnTask(task)
|
||||||
&& typeof task.worktree === "string" && task.worktree.length > 0)
|
&& typeof task.worktree === "string" && task.worktree.length > 0)
|
||||||
.map((task) => task.id);
|
.map((task) => task.id);
|
||||||
|
/*
|
||||||
|
FNXC:WorkflowScheduling 2026-08-01-01:05 (self-deadlock in the widened ledger, observed live):
|
||||||
|
A planned Ready card RETAINS its planning worktree for execution reuse, so counting it as a
|
||||||
|
holder must not block ITS OWN release — on release the slot TRANSFERS (the card executes in
|
||||||
|
the same worktree), it does not add. Without this exclusion the first unpause released only
|
||||||
|
2 of 4 slots' worth of work: the two remaining Ready cards were gated out by the very
|
||||||
|
worktrees they would reuse (2 wip + 3 idle-held = 5/4). Candidates in this set subtract
|
||||||
|
their own slot from the gate and skip the dispatch increment.
|
||||||
|
*/
|
||||||
|
const nonWipWorktreeHolderIdSet = new Set(nonWipWorktreeHolderIds);
|
||||||
let reservedWorktreeSlots = wipTaskIds.length + nonWipWorktreeHolderIds.length;
|
let reservedWorktreeSlots = wipTaskIds.length + nonWipWorktreeHolderIds.length;
|
||||||
let reservedConcurrentSlots = wipTaskIds.length;
|
let reservedConcurrentSlots = wipTaskIds.length;
|
||||||
const inProgressTaskIds = wipTaskIds;
|
const inProgressTaskIds = wipTaskIds;
|
||||||
@@ -2865,10 +2875,11 @@ export class Scheduler {
|
|||||||
store: this.store,
|
store: this.store,
|
||||||
tasks,
|
tasks,
|
||||||
});
|
});
|
||||||
|
const candidateHoldsWorktree = nonWipWorktreeHolderIdSet.has(task.id);
|
||||||
const concurrencyDiagnostic = computeConcurrencyGateDiagnostic({
|
const concurrencyDiagnostic = computeConcurrencyGateDiagnostic({
|
||||||
agentSlots: reservedConcurrentSlots,
|
agentSlots: reservedConcurrentSlots,
|
||||||
maxConcurrent,
|
maxConcurrent,
|
||||||
activeWorktrees: reservedWorktreeSlots,
|
activeWorktrees: reservedWorktreeSlots - (candidateHoldsWorktree ? 1 : 0),
|
||||||
maxWorktrees,
|
maxWorktrees,
|
||||||
worktreeHolderTaskIds: [...inProgressTaskIds, ...nonWipWorktreeHolderIds],
|
worktreeHolderTaskIds: [...inProgressTaskIds, ...nonWipWorktreeHolderIds],
|
||||||
semaphore: this.options.semaphore,
|
semaphore: this.options.semaphore,
|
||||||
@@ -2953,7 +2964,8 @@ export class Scheduler {
|
|||||||
task: freshTask,
|
task: freshTask,
|
||||||
});
|
});
|
||||||
|
|
||||||
reservedWorktreeSlots += 1;
|
// Transfer, not addition, for a candidate that already holds its worktree.
|
||||||
|
if (!candidateHoldsWorktree) reservedWorktreeSlots += 1;
|
||||||
reservedConcurrentSlots += 1;
|
reservedConcurrentSlots += 1;
|
||||||
let released = false;
|
let released = false;
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user