From 50383eb2bf249a5fc3b95138fa31c781ba09f850 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 05:54:02 +0200 Subject: [PATCH] fix(auto): honor solver swarm tool counts --- .../extensions/sf/auto/phases-unit.js | 23 +++++++++++-- .../phases-unit-zero-tool-guard.test.mjs | 34 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/resources/extensions/sf/tests/phases-unit-zero-tool-guard.test.mjs diff --git a/src/resources/extensions/sf/auto/phases-unit.js b/src/resources/extensions/sf/auto/phases-unit.js index 2a2b10f35..4c08b8a63 100644 --- a/src/resources/extensions/sf/auto/phases-unit.js +++ b/src/resources/extensions/sf/auto/phases-unit.js @@ -171,6 +171,20 @@ function clearDeferredCommitAfterCancelledUnit( ); } +/** + * Return the unit result that should drive the zero-tool-call guard. + * + * Purpose: preserve autonomous progress when a solver or repair pass produced + * the checkpoint after the first executor turn. The guard protects against + * genuine no-op turns, but it must not inspect a stale pre-solver result after + * currentUnitResult has been replaced with a later successful swarm turn. + * + * Consumer: runUnitPhase zero-tool-call guard after solver assessment. + */ +export function resultForZeroToolCallGuard(unitResult, currentUnitResult) { + return currentUnitResult ?? unitResult; +} + // ─── runUnitPhase ───────────────────────────────────────────────────────────── /** * Phase 4: Unit execution — dispatch prompt, await agent_end, closeout, artifact verify. @@ -1560,8 +1574,13 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) { // Swarm bypass: the ledger entry only reflects the parent session, which // never receives the subagent's tool calls. Use the real count surfaced by // runUnitViaSwarm (swarmToolCallCount) to avoid a false-positive retry. - const swarmRealToolCalls = unitResult.swarmToolCallCount ?? 0; - const isSwarmWithWork = unitResult._via === "swarm" && swarmRealToolCalls > 0; + const guardResult = resultForZeroToolCallGuard( + unitResult, + currentUnitResult, + ); + const swarmRealToolCalls = guardResult.swarmToolCallCount ?? 0; + const isSwarmWithWork = + guardResult._via === "swarm" && swarmRealToolCalls > 0; if (isSwarmWithWork) { debugLog("runUnitPhase", { phase: "zero-tool-calls-swarm-bypass", diff --git a/src/resources/extensions/sf/tests/phases-unit-zero-tool-guard.test.mjs b/src/resources/extensions/sf/tests/phases-unit-zero-tool-guard.test.mjs new file mode 100644 index 000000000..99983180c --- /dev/null +++ b/src/resources/extensions/sf/tests/phases-unit-zero-tool-guard.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { resultForZeroToolCallGuard } from "../auto/phases-unit.js"; + +test("zeroToolGuard_when_solver_pass_replaces_result_uses_current_result", () => { + const executorResult = { + status: "completed", + _via: "swarm", + swarmToolCallCount: 0, + }; + const solverResult = { + status: "completed", + _via: "swarm", + swarmToolCallCount: 1, + }; + + assert.equal( + resultForZeroToolCallGuard(executorResult, solverResult), + solverResult, + ); +}); + +test("zeroToolGuard_when_no_solver_result_uses_original_result", () => { + const executorResult = { + status: "completed", + _via: "swarm", + swarmToolCallCount: 0, + }; + + assert.equal( + resultForZeroToolCallGuard(executorResult, null), + executorResult, + ); +});