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" },
|
details: { agentId: "", state: "error" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:CapacityModel 2026-07-29-19:20 (PR #2579 review — greptile P1, TOCTOU):
|
FNXC:CapacityModel 2026-07-29-19:20 (PR #2579 review — greptile P1, TOCTOU):
|
||||||
RESERVE THE SLOT SYNCHRONOUSLY, before the first await.
|
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);
|
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 {
|
try {
|
||||||
// Create agent in AgentStore with reportsTo = parent task ID
|
// Create agent in AgentStore with reportsTo = parent task ID
|
||||||
const agent = await this.options.agentStore.createAgent({
|
const agent = await this.options.agentStore.createAgent({
|
||||||
|
|||||||
Reference in New Issue
Block a user