fix(engine): spawned children gate on maxWorktrees too — the spawn note promised both dimensions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/spawn-worktree-gate.md
Normal file
7
.changeset/spawn-worktree-gate.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Spawned child agents now count against the worktree cap, not only the agent cap.
|
||||
category: fix
|
||||
dev: fn_spawn_agent's own note said a child consumes both dimensions but gated only agents — a fan-out could exceed maxWorktrees to the agent limit. The worktree check runs after the synchronous slot reservation (preserving the anti-TOCTOU ordering, proven by the racing-spawns test) and unwinds it on refusal.
|
||||
@@ -21898,6 +21898,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
details: { agentId: "", state: "error" },
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:CapacityModel 2026-07-29-19:20 (PR #2579 review — greptile P1, TOCTOU):
|
||||
RESERVE THE SLOT SYNCHRONOUSLY, before the first await.
|
||||
@@ -21921,6 +21922,37 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
this.totalSpawnedCount = Math.max(0, this.totalSpawnedCount - 1);
|
||||
};
|
||||
|
||||
/*
|
||||
FNXC:CapacityModel 2026-08-01-02:40 (same class as the planning-admission gap, 374956ef23):
|
||||
The FNXC above says a child "consumes both configured dimensions" — and then gated only ONE.
|
||||
A child's worktree is not a task row, so the task-ledger gates never see it; count live
|
||||
children against the worktree budget here at the acquisition source, like planning admission
|
||||
now does. Runs AFTER the synchronous agent-slot reservation (its own TOCTOU rule: the awaits
|
||||
in this check must not reopen the two-racing-spawns hole — the reservation is already held,
|
||||
and a worktree refusal unwinds it). Absent/null maxWorktrees (worktrees off) falls through
|
||||
to the agent gate alone, matching every other lane.
|
||||
*/
|
||||
{
|
||||
const spawnMaxWorktrees = (settings as { maxWorktrees?: number | null }).maxWorktrees ?? 4;
|
||||
if (typeof spawnMaxWorktrees === "number" && Number.isFinite(spawnMaxWorktrees)) {
|
||||
const spawnTasks = await this.store.listTasks({ slim: true, includeArchived: false });
|
||||
const heldWorktrees = spawnTasks.filter((t) =>
|
||||
t.column !== "done" && t.column !== "archived"
|
||||
&& typeof t.worktree === "string" && t.worktree.length > 0).length;
|
||||
// totalSpawnedCount already includes THIS reservation; heldWorktrees covers task lanes.
|
||||
if (heldWorktrees + this.totalSpawnedCount > spawnMaxWorktrees) {
|
||||
releaseSpawnReservation();
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: `Worktree capacity reached (${heldWorktrees + this.totalSpawnedCount - 1}/${spawnMaxWorktrees} held, including spawned child agent(s)). Wait for work to finish, or raise Max Worktrees.`,
|
||||
}],
|
||||
details: { agentId: "", state: "error" },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Create agent in AgentStore with reportsTo = parent task ID
|
||||
const agent = await this.options.agentStore.createAgent({
|
||||
|
||||
Reference in New Issue
Block a user