Merge commit 'ef5dd7ba2511621b906abc54eb432ba79cf4195f'

This commit is contained in:
gsxdsm
2026-05-21 08:43:57 -07:00
4 changed files with 104 additions and 10 deletions

View File

@@ -33,8 +33,13 @@ declare global {
interface Window { interface Window {
__fusionDebug?: { __fusionDebug?: {
dashboardTraces?: { dashboardTraces?: {
get: typeof getTraces; get: () => unknown[];
clear: typeof clearTraces; clear: () => void;
};
resumeInstrumentation?: {
get: () => unknown[];
clear: () => void;
setEnabled: (nextEnabled: boolean) => void;
}; };
}; };
} }

View File

@@ -136,10 +136,14 @@ export function recordResumeEvent(
declare global { declare global {
interface Window { interface Window {
__fusionDebug?: { __fusionDebug?: {
dashboardTraces?: {
get: () => unknown[];
clear: () => void;
};
resumeInstrumentation?: { resumeInstrumentation?: {
get: typeof getResumeEvents; get: () => unknown[];
clear: typeof clearResumeEvents; clear: () => void;
setEnabled: typeof setResumeInstrumentationEnabled; setEnabled: (nextEnabled: boolean) => void;
}; };
}; };
} }

View File

@@ -431,7 +431,68 @@ describe("reviewStep — fallback retry for terminal unavailable", () => {
"FN-4092", "FN-4092",
expect.stringContaining("review retry with fallback model after UNAVAILABLE verdict"), expect.stringContaining("review retry with fallback model after UNAVAILABLE verdict"),
); );
expect(task.reviewerFallbackRetryCount).toBe(1); expect(task.reviewerFallbackRetryCount).toBe(0);
});
it("resets reviewerFallbackRetryCount to 0 after a successful review following prior fallbacks", async () => {
mockedCreateFnAgent
.mockResolvedValueOnce(createMockSession("No parseable verdict here."))
.mockResolvedValueOnce(createMockSession("### Verdict: APPROVE\n### Summary\nrecovered"));
const task = { id: "FN-4093", column: "in-progress", description: "d", dependencies: [], steps: [], currentStep: 0, log: [], prompt: "# prompt", createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", reviewerFallbackRetryCount: 3 };
const store = {
getSettings: vi.fn().mockResolvedValue({ maxReviewerFallbackRetries: 8, maxTotalRetriesBeforeFail: 25 }),
getTask: vi.fn().mockImplementation(async () => task),
updateTask: vi.fn().mockImplementation(async (_id: string, patch: Record<string, unknown>) => Object.assign(task, patch)),
logEntry: vi.fn().mockResolvedValue(undefined),
appendAgentLog: vi.fn().mockResolvedValue(undefined),
};
const result = await reviewStep(
"/tmp/worktree", "FN-4093", 2, "Retry", "plan", "# prompt", undefined,
{
store: store as any,
taskId: "FN-4093",
projectValidatorFallbackProvider: "openai",
projectValidatorFallbackModelId: "gpt-5-mini",
},
);
expect(result.verdict).toBe("APPROVE");
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2);
expect(store.updateTask).toHaveBeenCalledWith("FN-4093", { reviewerFallbackRetryCount: 0 });
expect(task.reviewerFallbackRetryCount).toBe(0);
});
it("does not reset reviewerFallbackRetryCount when fallback remains UNAVAILABLE", async () => {
mockedCreateFnAgent
.mockResolvedValueOnce(createMockSession("No parseable verdict #1"))
.mockResolvedValueOnce(createMockSession("No parseable verdict #2"));
const task = { id: "FN-4094", column: "in-progress", description: "d", dependencies: [], steps: [], currentStep: 0, log: [], prompt: "# prompt", createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", reviewerFallbackRetryCount: 2 };
const store = {
getSettings: vi.fn().mockResolvedValue({ maxReviewerFallbackRetries: 8, maxTotalRetriesBeforeFail: 25 }),
getTask: vi.fn().mockImplementation(async () => task),
updateTask: vi.fn().mockImplementation(async (_id: string, patch: Record<string, unknown>) => Object.assign(task, patch)),
logEntry: vi.fn().mockResolvedValue(undefined),
appendAgentLog: vi.fn().mockResolvedValue(undefined),
};
const result = await reviewStep(
"/tmp/worktree", "FN-4094", 2, "Retry", "plan", "# prompt", undefined,
{
store: store as any,
taskId: "FN-4094",
projectValidatorFallbackProvider: "openai",
projectValidatorFallbackModelId: "gpt-5-mini",
},
);
expect(result.verdict).toBe("UNAVAILABLE");
expect(store.updateTask).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ reviewerFallbackRetryCount: 0 }),
);
}); });
it("retries once after non-context reviewer error", async () => { it("retries once after non-context reviewer error", async () => {

View File

@@ -662,6 +662,13 @@ export async function reviewStep(
const hasConfiguredFallback = Boolean(validatorFallbackProvider && validatorFallbackModelId); const hasConfiguredFallback = Boolean(validatorFallbackProvider && validatorFallbackModelId);
const retrySettings = liveSettings ?? options.settings; const retrySettings = liveSettings ?? options.settings;
const resetReviewerFallbackRetryCount = async (): Promise<void> => {
if (!options.store || !options.taskId || typeof options.store.updateTask !== "function") {
return;
}
await options.store.updateTask(options.taskId, { reviewerFallbackRetryCount: 0 }).catch(() => undefined);
};
let firstAttempt: { verdict: ReviewVerdict; summary: string; review: string }; let firstAttempt: { verdict: ReviewVerdict; summary: string; review: string };
try { try {
firstAttempt = await runAttempt(request); firstAttempt = await runAttempt(request);
@@ -680,10 +687,14 @@ export async function reviewStep(
}); });
} }
try { try {
return await runAttempt(request, { const fallbackResult = await runAttempt(request, {
forceProvider: validatorFallbackProvider, forceProvider: validatorFallbackProvider,
forceModelId: validatorFallbackModelId, forceModelId: validatorFallbackModelId,
}); });
if (fallbackResult.verdict !== "UNAVAILABLE") {
await resetReviewerFallbackRetryCount();
}
return fallbackResult;
} catch { } catch {
throw err; throw err;
} }
@@ -702,13 +713,18 @@ export async function reviewStep(
}); });
} }
try { try {
return await runAttempt(fallbackReviewRequest); const fallbackResult = await runAttempt(fallbackReviewRequest);
if (fallbackResult.verdict !== "UNAVAILABLE") {
await resetReviewerFallbackRetryCount();
}
return fallbackResult;
} catch { } catch {
throw err; throw err;
} }
} }
if (firstAttempt.verdict !== "UNAVAILABLE") { if (firstAttempt.verdict !== "UNAVAILABLE") {
await resetReviewerFallbackRetryCount();
return firstAttempt; return firstAttempt;
} }
@@ -725,10 +741,14 @@ export async function reviewStep(
agentId: options.agentId, agentId: options.agentId,
}); });
} }
return runAttempt(request, { const fallbackResult = await runAttempt(request, {
forceProvider: validatorFallbackProvider, forceProvider: validatorFallbackProvider,
forceModelId: validatorFallbackModelId, forceModelId: validatorFallbackModelId,
}); });
if (fallbackResult.verdict !== "UNAVAILABLE") {
await resetReviewerFallbackRetryCount();
}
return fallbackResult;
} }
await logFallbackRetry("UNAVAILABLE verdict", "same-model strict prompt"); await logFallbackRetry("UNAVAILABLE verdict", "same-model strict prompt");
@@ -743,7 +763,11 @@ export async function reviewStep(
agentId: options.agentId, agentId: options.agentId,
}); });
} }
return runAttempt(fallbackReviewRequest); const fallbackResult = await runAttempt(fallbackReviewRequest);
if (fallbackResult.verdict !== "UNAVAILABLE") {
await resetReviewerFallbackRetryCount();
}
return fallbackResult;
} }
function isReviewerSessionReuseError(error: unknown): boolean { function isReviewerSessionReuseError(error: unknown): boolean {