Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/resources/extensions/gsd/auto/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,17 @@
finishTurn("stopped", "execution", "unit-break");
break;
}
if (unitPhaseResult.action === "retry") {
finishIncompleteIteration({
status: "retry",
reason: unitPhaseResult.reason,
retry: true,
unitType: iterData.unitType,
unitId: iterData.unitId,
});
finishTurn("retry", "execution", unitPhaseResult.reason);
continue;
}

// ── Verify first, then reconcile (only mark complete on pass) ──
debugLog("autoLoop", { phase: "custom-engine-verify", iteration, unitId: iterData.unitId });
Expand Down Expand Up @@ -798,13 +809,13 @@
continue;
}
const preData = preDispatchResult.data;
const guardsResult = await runGuards(ic, preData.mid);

Check failure on line 812 in src/resources/extensions/gsd/auto/loop.ts

View workflow job for this annotation

GitHub Actions / build

'preData' is possibly 'undefined'.
phaseReporter.report("guard", guardsResult.action);
if (guardsResult.action === "break") {
finishTurn("stopped", "manual-attention", "guard-break");
break;
}
const dispatchResult = await runDispatch(ic, preData, loopState);

Check failure on line 818 in src/resources/extensions/gsd/auto/loop.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'PreDispatchData | undefined' is not assignable to parameter of type 'PreDispatchData'.
phaseReporter.report("dispatch", dispatchResult.action);
if (dispatchResult.action === "break") {
finishTurn("stopped", "manual-attention", "dispatch-break");
Expand All @@ -814,7 +825,7 @@
finishTurn("skipped");
continue;
}
iterData = dispatchResult.data;

Check failure on line 828 in src/resources/extensions/gsd/auto/loop.ts

View workflow job for this annotation

GitHub Actions / build

Type 'IterationData | undefined' is not assignable to type 'IterationData'.
observedUnitType = iterData.unitType;
observedUnitId = iterData.unitId;
}
Expand Down Expand Up @@ -963,6 +974,21 @@
finishTurn("stopped", "execution", "unit-break");
break;
}
if (unitPhaseResult.action === "retry") {
dispatchSettled = settleDispatchFailed(dispatchId, unitPhaseResult.reason, {
markFailed: markDispatchFailed,
logWriteFailure: logDispatchLedgerWriteFailure,
}) || dispatchSettled;
finishIncompleteIteration({
status: "retry",
reason: unitPhaseResult.reason,
retry: true,
unitType: iterData.unitType,
unitId: iterData.unitId,
});
finishTurn("retry", "execution", unitPhaseResult.reason);
continue;
}

// ── Phase 5: Finalize ───────────────────────────────────────────────

Expand Down
11 changes: 8 additions & 3 deletions src/resources/extensions/gsd/auto/phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2370,9 +2370,14 @@ export async function runUnitPhase(
`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry`,
"warning",
);
// Fall through to next iteration where dispatch will re-derive
// and re-dispatch this unit.
return { action: "next", data: { unitStartedAt: _resolveCurrentUnitStartedAtForTest(s.currentUnit), requestDispatchedAt: unitResult.requestDispatchedAt } };
return {
action: "retry",
reason: "zero-tool-calls",
data: {
unitStartedAt: _resolveCurrentUnitStartedAtForTest(s.currentUnit),
requestDispatchedAt: unitResult.requestDispatchedAt,
},
};
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/resources/extensions/gsd/auto/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface UnitResult {
export type PhaseResult<T = void> =
| { action: "continue" }
| { action: "break"; reason: string }
| { action: "retry"; reason: string; data?: T }
| { action: "next"; data: T }

export interface IterationContext {
Expand Down
36 changes: 20 additions & 16 deletions src/resources/extensions/gsd/tests/auto-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3123,7 +3123,7 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
const pi = makeMockPi();

let iterationCount = 0;
let closeoutCount = 0;
const notifications: string[] = [];
ctx.ui.notify = (msg: string) => { notifications.push(msg); };

Expand Down Expand Up @@ -3158,6 +3158,7 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
};
},
closeoutUnit: async () => {
closeoutCount++;
// Simulate snapshotUnitMetrics adding a 0-toolCalls entry to ledger
mockLedger.units.push({
type: "execute-task",
Expand All @@ -3168,15 +3169,9 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
tokens: { input: 100, output: 200, total: 300, cacheRead: 0, cacheWrite: 0 },
cost: 0.50,
});
if (closeoutCount >= 2) s.active = false;
},
getLedger: () => mockLedger,
postUnitPostVerification: async () => {
deps.callLog.push("postUnitPostVerification");
iterationCount++;
// Deactivate after 2nd iteration
s.active = iterationCount < 2;
return "continue" as const;
},
});

const loopPromise = autoLoop(ctx, pi, s, deps);
Expand All @@ -3189,6 +3184,7 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
await new Promise((r) => setTimeout(r, 50));
mockLedger.units.length = 0; // clear previous entry
(deps as any).closeoutUnit = async () => {
closeoutCount++;
mockLedger.units.push({
type: "execute-task",
id: "M001/S01/T01",
Expand All @@ -3198,6 +3194,7 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
tokens: { input: 500, output: 800, total: 1300, cacheRead: 0, cacheWrite: 0 },
cost: 1.00,
});
if (closeoutCount >= 2) s.active = false;
};
resolveAgentEnd(makeEvent());

Expand All @@ -3219,6 +3216,11 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
deriveCount >= 2,
`deriveState should be called at least 2 times for retry (got ${deriveCount})`,
);
assert.equal(
deps.callLog.filter((c) => c === "postUnitPreVerification").length,
1,
"zero-tool retry should bypass finalize on the failed iteration",
);
});

test("autoLoop pauses user-driven deep question instead of flagging 0 tool calls", async () => {
Expand Down Expand Up @@ -3310,7 +3312,7 @@ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2
ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
const pi = makeMockPi();

let iterationCount = 0;
let closeoutCount = 0;
const notifications: string[] = [];
ctx.ui.notify = (msg: string) => { notifications.push(msg); };

Expand Down Expand Up @@ -3344,6 +3346,7 @@ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2
};
},
closeoutUnit: async () => {
closeoutCount++;
// complete-slice with 0 tool calls — context exhausted, no progress
mockLedger.units.push({
type: "complete-slice",
Expand All @@ -3354,15 +3357,9 @@ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2
tokens: { input: 50, output: 100, total: 150, cacheRead: 0, cacheWrite: 0 },
cost: 0.10,
});
if (closeoutCount >= 2) s.active = false;
},
getLedger: () => mockLedger,
postUnitPostVerification: async () => {
deps.callLog.push("postUnitPostVerification");
iterationCount++;
// Deactivate after 2nd iteration
s.active = iterationCount < 2;
return "continue" as const;
},
});

const loopPromise = autoLoop(ctx, pi, s, deps);
Expand All @@ -3375,6 +3372,7 @@ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2
await new Promise((r) => setTimeout(r, 50));
mockLedger.units.length = 0;
(deps as any).closeoutUnit = async () => {
closeoutCount++;
mockLedger.units.push({
type: "complete-slice",
id: "M001/S01",
Expand All @@ -3384,6 +3382,7 @@ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2
tokens: { input: 200, output: 400, total: 600, cacheRead: 0, cacheWrite: 0 },
cost: 0.30,
});
if (closeoutCount >= 2) s.active = false;
};
resolveAgentEnd(makeEvent());

Expand All @@ -3404,6 +3403,11 @@ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2
deriveCount >= 2,
`deriveState should be called at least 2 times for retry (got ${deriveCount})`,
);
assert.equal(
deps.callLog.filter((c) => c === "postUnitPreVerification").length,
1,
"zero-tool retry should bypass finalize on the failed iteration",
);
});

// ─── Worktree health check (#1833) ────────────────────────────────────────
Expand Down
Loading