feat(FN-5347): reset manual retry counters across task retry surfaces

- Add a shared manual retry reset helper in core and export it for consumers
- Wire retry counter resets into CLI task retry, extension task retry, and dashboard task workflow routes
- Align retry reset behavior with task typing updates and remove superseded task-helper reset paths
- Add focused core/CLI/extension/dashboard tests plus docs and a changeset describing retry reset behavior
This commit is contained in:
Fusion (runfusion.ai)
2026-05-21 15:45:48 -07:00
committed by gsxdsm
parent a0aa6c8c5b
commit 216c32bfe5
13 changed files with 188 additions and 90 deletions

View File

@@ -0,0 +1,38 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { buildManualRetryResetPatch, MANUAL_RETRY_RESET_COUNTER_KEYS } from "../manual-retry-reset.js";
const RETRY_SUMMARY_COUNTER_REGEX = /toCount\(task\.(\w+)\)/g;
describe("buildManualRetryResetPatch", () => {
it("resets all manual retry counters to zero", () => {
const patch = buildManualRetryResetPatch();
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
expect(patch[key]).toBe(0);
}
});
it("includes all retry-summary counters in the reset key list", () => {
const retrySummarySource = readFileSync(new URL("../retry-summary.ts", import.meta.url), "utf-8");
const retrySummaryKeys = new Set<string>();
let match: RegExpExecArray | null = RETRY_SUMMARY_COUNTER_REGEX.exec(retrySummarySource);
while (match) {
retrySummaryKeys.add(match[1]);
match = RETRY_SUMMARY_COUNTER_REGEX.exec(retrySummarySource);
}
for (const key of retrySummaryKeys) {
expect(MANUAL_RETRY_RESET_COUNTER_KEYS).toContain(key);
}
});
it("sets mergeRetries only when requested", () => {
expect(buildManualRetryResetPatch()).not.toHaveProperty("mergeRetries");
expect(buildManualRetryResetPatch({ resetMergeRetries: true })).toMatchObject({ mergeRetries: 0 });
});
it("clears nextRecoveryAt", () => {
expect(buildManualRetryResetPatch()).toMatchObject({ nextRecoveryAt: null });
});
});

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { buildManualRetryResetPatch, getPrimaryPrInfo } from "../task-helpers.js";
import { getPrimaryPrInfo } from "../task-helpers.js";
describe("getPrimaryPrInfo", () => {
it("returns prInfo when only legacy field is set", () => {
const prInfo = { number: 1 } as any;
@@ -23,12 +24,3 @@ describe("getPrimaryPrInfo", () => {
});
});
describe("buildManualRetryResetPatch", () => {
it("resets only manual retry counters", () => {
expect(buildManualRetryResetPatch()).toEqual({
taskDoneRetryCount: 0,
workflowStepRetries: 0,
stuckKillCount: 0,
});
});
});

View File

@@ -201,7 +201,8 @@ export {
hasTitleIdDrift,
normalizeTitleForTaskId,
} from "./task-title-id-drift.js";
export { getPrimaryPrInfo, buildManualRetryResetPatch } from "./task-helpers.js";
export { getPrimaryPrInfo } from "./task-helpers.js";
export { MANUAL_RETRY_RESET_COUNTER_KEYS, buildManualRetryResetPatch } from "./manual-retry-reset.js";
export type {
TaskIdIntegrityAnomaly,
TaskIdIntegrityAnomalyKind,

View File

@@ -0,0 +1,33 @@
import type { Task } from "./types.js";
export const MANUAL_RETRY_RESET_COUNTER_KEYS = [
"stuckKillCount",
"recoveryRetryCount",
"taskDoneRetryCount",
"worktreeSessionRetryCount",
"workflowStepRetries",
"verificationFailureCount",
"postReviewFixCount",
"mergeConflictBounceCount",
"branchConflictRecoveryCount",
"reviewerContextRetryCount",
"reviewerFallbackRetryCount",
"completionHandoffLimboRecoveryCount",
"mergeAuditBounceCount",
] as const satisfies ReadonlyArray<keyof Task>;
export function buildManualRetryResetPatch(options?: { resetMergeRetries?: boolean }): Partial<Task> {
const patch: Partial<Task> = {
nextRecoveryAt: null as unknown as Task["nextRecoveryAt"],
};
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
patch[key] = 0;
}
if (options?.resetMergeRetries) {
patch.mergeRetries = 0;
}
return patch;
}

View File

@@ -3,11 +3,3 @@ import type { PrInfo, Task } from "./types.js";
export function getPrimaryPrInfo(task: Pick<Task, "prInfo" | "prInfos">): PrInfo | undefined {
return task.prInfos?.[0] ?? task.prInfo;
}
export function buildManualRetryResetPatch(): Pick<Task, "taskDoneRetryCount" | "workflowStepRetries" | "stuckKillCount"> {
return {
taskDoneRetryCount: 0,
workflowStepRetries: 0,
stuckKillCount: 0,
};
}