Merge commit 'ef5dd7ba2511621b906abc54eb432ba79cf4195f'
This commit is contained in:
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user