Repository navigation
fix(swarm): release domain-pool agents after terminal tasks - #4247
Merged
ruvnet merged 2 commits intoOct 17, 2026
Merged
Conversation
assignTaskToDomain() acquires an AgentPool slot, but handleTaskComplete(), terminal handleTaskFail() and cancelTask() only reset AgentState. releaseAgentToDomain() had no callers, so each finished domain task permanently consumed a slot and domainTaskQueues never drained. Release the pool-owned agent on terminal transitions and dispatch the next queued task. Skip terminal queue entries and avoid scaling a one-agent domain before its queued task is dispatched.
…lict unified-coordinator.ts conflicted in three spots: ruvnet#4244 (already on main) replaced the "capture task.status in a wasTerminal var, mutate unconditionally" pattern with an upfront `!isTaskSettled(task)` guard that skips the whole handler body for an already-settled task (fixing a double-processing bug the old pattern had: duplicate metric increments and duplicate emitted events for a late/duplicate message). This PR's `wasTerminal` captures were reading task.status at a point where main's guard already guarantees non-terminal, so they were always false/true trivially. Resolution: keep main's guard/fencing behavior as-is, drop the now-redundant `wasTerminal` variable, and make the `releaseDomainAgent` calls unconditional within the already-guarded block — preserving this PR's domain-pool-release fix without reintroducing the double-processing bug ruvnet#4244 fixed. Verified: full @claude-flow/swarm test suite (26 files, 321 tests) passes, including coordinator.cancellation.test.ts and domain-pool-agent-release.test.ts from both lineages. Co-Authored-By: RuFlo <ruv@ruv.net>
ruvnet
added a commit
to rudycelekli/agent-forge
that referenced
this pull request
Oct 17, 2026
Textual conflict was only in agent-pool.ts's scale() (this PR's reconcileReservations() call vs main's autoScale early-return from ruvnet#4246) — resolved by running reconcileReservations() first, matching the pattern every other public method in the file already follows. A second, non-textual regression surfaced under test: this PR makes AgentPool.getBusyAgents()/getAvailableAgents() self-heal their busy/available sets from the live AgentState.status on every read (reconcileReservations). unified-coordinator.ts's releaseDomainAgent() (from ruvnet#4247, already on main) used getBusyAgents() to check whether the pool actually owns an agent before releasing it — but by the time that fire-and-forget release runs, the caller has already reset agent.status to 'idle', so the reconciled getBusyAgents() always reports the agent as already-available and the function returns early, skipping the release and queue-drain entirely. Fixed by checking pool membership via getAgent() instead, which isn't affected by the busy/available reconciliation. Verified: full @claude-flow/swarm test suite, 27 files / 332 tests, all pass — including both PRs' own regression tests (agent-pool.shared-reservations.test.ts, domain-pool-agent-release.test.ts). Co-Authored-By: RuFlo <ruv@ruv.net>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and change
UnifiedSwarmCoordinator.assignTaskToDomain()acquires its worker throughAgentPool.acquire(), which moves that agent fromavailabletobusy. The terminal task paths only reset the sharedAgentState:handleTaskComplete()setsagent.status = 'idle'/currentTask = undefinedhandleTaskFail()'s final-failure branch does the samecancelTask()does the sameNone of them call
releaseAgentToDomain().AgentPool.release()is the only operation that removes the id frombusyand returns it toavailable;releaseAgentToDomain()is also the only code that drainsdomainTaskQueues. It had zero callers insrc/(git grep at1882006a0;git log -S releaseAgentToDomainshows only the introducing commit53cfd8a53).Consequences in the documented API flow (README
assignTaskToDomain()/executeParallel(), andQueenCoordinator.executeDelegation()):pool.acquire()returnsundefinedand further tasks sit indomainTaskQueues;getStatus()showsavailableAgents: 0/tasksQueuedgrowing andexecuteParallel()waits until its task timeout.Change: release the acquired domain agent after a non-terminal to terminal transition, on completion, terminal failure, and cancellation. The release also assigns the next queued task for that domain. Retries are deliberately unchanged:
handleTaskFail()re-queues throughassignTask(), which may hand the task back to the same agent, so releasing there would race that reassignment.Two drain-path guards are part of the same change because making
release()reachable exposes both failure modes:AgentPool.release()normally callscheckScaling(), and a one-agent domain (queen,maxSize: 1) would scale to zero before the queued task can be dispatched; the nextacquire()would then create a pool-only agent absent from coordinator state.release()accepts an optional{ rescale: false }, used when an assignable task is waiting, so the freed slot is handed to that task first.domainTaskQueues. The drain now skips any entry whose status is no longerqueued/created, so releasing a slot cannot resurrect a terminal task and send it to a worker.Validation
v3/@claude-flow/swarm/__tests__/domain-pool-agent-release.test.ts.1882006a0: 6/6 fail (the busy set retains the completed/failed/cancelled agent; queued tasks stayqueued; the one-agent queen case is assigned to a pool-only id).@claude-flow/swarmsuite 283/283, 19/19 files;tsc --noEmit0 errors;git diff --checkclean.Relationship to #4244
#4244 (
fix(swarm): keep coordinator cancellation terminal) touches the same handlers, but this change is independent on current main: it includeswasTerminalguards so a late/duplicate terminal report cannot free an agent that has since been reassigned. It does not fix the cancellation-retry race that #4244 addresses. If #4244 lands first, the guards here can be narrowed during rebase; if this lands first, #4244 can keep its terminal-state logic.Scope / known limitations
AgentPool.acquire()can create a pool-only agent that is not registered in coordinator state; the one-agent queue path is guarded here, but the general empty-pool acquisition path is a separate fix in fix(swarm): stop domain pools from creating unregistered agents #4246.submitTask()auto-assignment followed byassignTaskToDomain()can leave the first generic agent busy; separate follow-up.waitForTaskCompletion()/waitForQueuedTask()) is a client-side deadline and still does not release; releasing there would double-book an agent that may still be working. Not changed.@claude-flow/swarmis a standalone leaf package (not a dependency of@claude-flow/cli); blast radius is the published package and its consumers, matching the scope of prior merged swarm fixes dream(swarm): #3538 fix spawnAgent() auto-domain branch skipping pool.add() (evaluated, ACCEPT-scoped) #3539 / dream(swarm): #3242 fix AgentPool health checks that could never detect a dead agent (evaluated, ACCEPT-scoped) #3243.Compatibility
AgentPool.release()gains an optional{ rescale?: boolean }parameter; existing calls behave exactly as before.releaseAgentToDomain()behavior is unchanged for callers; the newreleaseDomainAgent()helper only calls it for agents the pool actually owns.SwarmEventType:agent.domain_release_failed, emitted only when pool bookkeeping fails; task completion/failure still resolves.Interaction with the domain-pool no-autoscale fix
If the separate
fix(swarm): stop domain pools from creating unregistered agentschange lands first,checkScaling()becomes a no-op for domain pools, so the{ rescale: false }parameter added here is redundant. A rebase can drop that parameter and the correspondingagent-pool.ts/types.tshunks; no conflict is expected.