feat(FN-5443): merge fusion/fn-5443
This commit is contained in:
@@ -2349,7 +2349,13 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
|||||||
});
|
});
|
||||||
await store.moveTask(task.id, "in-progress");
|
await store.moveTask(task.id, "in-progress");
|
||||||
await store.moveTask(task.id, "in-review");
|
await store.moveTask(task.id, "in-review");
|
||||||
await store.updateTask(task.id, { status: "failed", error: "429 rate limited" });
|
await store.updateTask(task.id, {
|
||||||
|
status: "failed",
|
||||||
|
error: "429 rate limited",
|
||||||
|
taskDoneRetryCount: 2,
|
||||||
|
workflowStepRetries: 3,
|
||||||
|
stuckKillCount: 4,
|
||||||
|
});
|
||||||
|
|
||||||
const retryTool = api.tools.get("fn_task_retry")!;
|
const retryTool = api.tools.get("fn_task_retry")!;
|
||||||
const result = await retryTool.execute("retry-exec", { id: task.id }, undefined, undefined, makeCtx(tmpDir));
|
const result = await retryTool.execute("retry-exec", { id: task.id }, undefined, undefined, makeCtx(tmpDir));
|
||||||
@@ -2362,6 +2368,9 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
|||||||
expect(updated?.status).toBeFalsy();
|
expect(updated?.status).toBeFalsy();
|
||||||
expect(updated?.error).toBeFalsy();
|
expect(updated?.error).toBeFalsy();
|
||||||
expect(updated?.steps[1].status).toBe("in-progress");
|
expect(updated?.steps[1].status).toBe("in-progress");
|
||||||
|
expect(updated?.taskDoneRetryCount).toBe(0);
|
||||||
|
expect(updated?.workflowStepRetries).toBe(0);
|
||||||
|
expect(updated?.stuckKillCount).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves zero-step execution-failed in-review task to todo and clears failure state", async () => {
|
it("moves zero-step execution-failed in-review task to todo and clears failure state", async () => {
|
||||||
@@ -2410,7 +2419,14 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
|||||||
});
|
});
|
||||||
await store.moveTask(task.id, "in-progress");
|
await store.moveTask(task.id, "in-progress");
|
||||||
await store.moveTask(task.id, "in-review");
|
await store.moveTask(task.id, "in-review");
|
||||||
await store.updateTask(task.id, { status: "failed", error: "merge conflict", mergeRetries: 3 });
|
await store.updateTask(task.id, {
|
||||||
|
status: "failed",
|
||||||
|
error: "merge conflict",
|
||||||
|
mergeRetries: 3,
|
||||||
|
taskDoneRetryCount: 5,
|
||||||
|
workflowStepRetries: 4,
|
||||||
|
stuckKillCount: 7,
|
||||||
|
});
|
||||||
|
|
||||||
const retryTool = api.tools.get("fn_task_retry")!;
|
const retryTool = api.tools.get("fn_task_retry")!;
|
||||||
const result = await retryTool.execute("retry-merge", { id: task.id }, undefined, undefined, makeCtx(tmpDir));
|
const result = await retryTool.execute("retry-merge", { id: task.id }, undefined, undefined, makeCtx(tmpDir));
|
||||||
@@ -2423,6 +2439,9 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
|||||||
expect(updated?.status).toBeFalsy();
|
expect(updated?.status).toBeFalsy();
|
||||||
expect(updated?.error).toBeFalsy();
|
expect(updated?.error).toBeFalsy();
|
||||||
expect(updated?.mergeRetries).toBe(0);
|
expect(updated?.mergeRetries).toBe(0);
|
||||||
|
expect(updated?.taskDoneRetryCount).toBe(0);
|
||||||
|
expect(updated?.workflowStepRetries).toBe(0);
|
||||||
|
expect(updated?.stuckKillCount).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps zero-step merge-failed in-review task with prior merge attempts in-review and resets merge state", async () => {
|
it("keeps zero-step merge-failed in-review task with prior merge attempts in-review and resets merge state", async () => {
|
||||||
|
|||||||
@@ -2415,6 +2415,9 @@ describe("runTaskRetry", () => {
|
|||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
|
stuckKillCount: 0,
|
||||||
});
|
});
|
||||||
expect(mockMoveTask).toHaveBeenCalledWith("FN-001", "todo");
|
expect(mockMoveTask).toHaveBeenCalledWith("FN-001", "todo");
|
||||||
expect(mockLogEntry).toHaveBeenCalledWith("FN-001", "Retry requested from CLI", "Task reset to todo for retry");
|
expect(mockLogEntry).toHaveBeenCalledWith("FN-001", "Retry requested from CLI", "Task reset to todo for retry");
|
||||||
@@ -2475,6 +2478,9 @@ describe("runTaskRetry", () => {
|
|||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
|
stuckKillCount: 0,
|
||||||
});
|
});
|
||||||
expect(mockMoveTask).toHaveBeenCalledWith("FN-001", "todo");
|
expect(mockMoveTask).toHaveBeenCalledWith("FN-001", "todo");
|
||||||
expect(mockLogEntry).toHaveBeenCalledWith("FN-001", "Retry requested from CLI", "Task reset to todo for retry");
|
expect(mockLogEntry).toHaveBeenCalledWith("FN-001", "Retry requested from CLI", "Task reset to todo for retry");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TaskStore, COLUMNS, COLUMN_LABELS, CentralCore, extractIntentSignature, findNearDuplicates, getTaskDuplicateLineage, reconcileDeterministicDuplicate, runDeterministicDuplicateGuard, type Settings, type Column, type StepStatus, type AgentLogType, type AgentLogEntry, type IntentSignature, type NearDuplicateCandidate, type NearDuplicateMatch } from "@fusion/core";
|
import { TaskStore, COLUMNS, COLUMN_LABELS, CentralCore, buildManualRetryResetPatch, extractIntentSignature, findNearDuplicates, getTaskDuplicateLineage, reconcileDeterministicDuplicate, runDeterministicDuplicateGuard, type Settings, type Column, type StepStatus, type AgentLogType, type AgentLogEntry, type IntentSignature, type NearDuplicateCandidate, type NearDuplicateMatch } from "@fusion/core";
|
||||||
import { aiMergeTask } from "@fusion/engine";
|
import { aiMergeTask } from "@fusion/engine";
|
||||||
import { createInterface } from "node:readline/promises";
|
import { createInterface } from "node:readline/promises";
|
||||||
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||||
@@ -1011,6 +1011,7 @@ export async function runTaskRetry(id: string, projectName?: string) {
|
|||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
|
...buildManualRetryResetPatch(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move to todo column
|
// Move to todo column
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
TaskStore,
|
TaskStore,
|
||||||
COLUMNS,
|
COLUMNS,
|
||||||
COLUMN_LABELS,
|
COLUMN_LABELS,
|
||||||
|
buildManualRetryResetPatch,
|
||||||
validateNodeOverrideChange,
|
validateNodeOverrideChange,
|
||||||
type Task,
|
type Task,
|
||||||
type InsightCategory,
|
type InsightCategory,
|
||||||
@@ -999,7 +1000,11 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
hasIncompleteSteps || (task.steps.length === 0 && (task.mergeRetries ?? 0) === 0);
|
hasIncompleteSteps || (task.steps.length === 0 && (task.mergeRetries ?? 0) === 0);
|
||||||
|
|
||||||
if (isExecutionFailureInReview) {
|
if (isExecutionFailureInReview) {
|
||||||
await store.updateTask(params.id, { status: null, error: null, stuckKillCount: 0 });
|
await store.updateTask(params.id, {
|
||||||
|
status: null,
|
||||||
|
error: null,
|
||||||
|
...buildManualRetryResetPatch(),
|
||||||
|
});
|
||||||
await store.logEntry(params.id, "Retry requested via Fusion extension (execution failure in-review → todo, preserving progress)");
|
await store.logEntry(params.id, "Retry requested via Fusion extension (execution failure in-review → todo, preserving progress)");
|
||||||
await store.moveTask(params.id, "todo", { preserveProgress: true });
|
await store.moveTask(params.id, "todo", { preserveProgress: true });
|
||||||
return {
|
return {
|
||||||
@@ -1008,7 +1013,12 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await store.updateTask(params.id, { status: null, error: null, stuckKillCount: 0, mergeRetries: 0 });
|
await store.updateTask(params.id, {
|
||||||
|
status: null,
|
||||||
|
error: null,
|
||||||
|
...buildManualRetryResetPatch(),
|
||||||
|
mergeRetries: 0,
|
||||||
|
});
|
||||||
await store.logEntry(params.id, "Retry requested via Fusion extension (in-review merge retry, mergeRetries reset)");
|
await store.logEntry(params.id, "Retry requested via Fusion extension (in-review merge retry, mergeRetries reset)");
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Retried ${params.id} → in-review (merge retry state cleared)` }],
|
content: [{ type: "text", text: `Retried ${params.id} → in-review (merge retry state cleared)` }],
|
||||||
@@ -1017,7 +1027,11 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear failure state and move to todo for other columns
|
// Clear failure state and move to todo for other columns
|
||||||
await store.updateTask(params.id, { status: null, error: null });
|
await store.updateTask(params.id, {
|
||||||
|
status: null,
|
||||||
|
error: null,
|
||||||
|
...buildManualRetryResetPatch(),
|
||||||
|
});
|
||||||
|
|
||||||
// Move to todo column
|
// Move to todo column
|
||||||
await store.moveTask(params.id, 'todo');
|
await store.moveTask(params.id, 'todo');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getPrimaryPrInfo } from "../task-helpers.js";
|
import { buildManualRetryResetPatch, getPrimaryPrInfo } from "../task-helpers.js";
|
||||||
|
|
||||||
describe("getPrimaryPrInfo", () => {
|
describe("getPrimaryPrInfo", () => {
|
||||||
it("returns prInfo when only legacy field is set", () => {
|
it("returns prInfo when only legacy field is set", () => {
|
||||||
const prInfo = { number: 1 } as any;
|
const prInfo = { number: 1 } as any;
|
||||||
@@ -23,3 +22,13 @@ describe("getPrimaryPrInfo", () => {
|
|||||||
expect(getPrimaryPrInfo({})).toBeUndefined();
|
expect(getPrimaryPrInfo({})).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("buildManualRetryResetPatch", () => {
|
||||||
|
it("resets only manual retry counters", () => {
|
||||||
|
expect(buildManualRetryResetPatch()).toEqual({
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
|
stuckKillCount: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export {
|
|||||||
hasTitleIdDrift,
|
hasTitleIdDrift,
|
||||||
normalizeTitleForTaskId,
|
normalizeTitleForTaskId,
|
||||||
} from "./task-title-id-drift.js";
|
} from "./task-title-id-drift.js";
|
||||||
export { getPrimaryPrInfo } from "./task-helpers.js";
|
export { getPrimaryPrInfo, buildManualRetryResetPatch } from "./task-helpers.js";
|
||||||
export type {
|
export type {
|
||||||
TaskIdIntegrityAnomaly,
|
TaskIdIntegrityAnomaly,
|
||||||
TaskIdIntegrityAnomalyKind,
|
TaskIdIntegrityAnomalyKind,
|
||||||
|
|||||||
@@ -3,3 +3,11 @@ import type { PrInfo, Task } from "./types.js";
|
|||||||
export function getPrimaryPrInfo(task: Pick<Task, "prInfo" | "prInfos">): PrInfo | undefined {
|
export function getPrimaryPrInfo(task: Pick<Task, "prInfo" | "prInfos">): PrInfo | undefined {
|
||||||
return task.prInfos?.[0] ?? task.prInfo;
|
return task.prInfos?.[0] ?? task.prInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildManualRetryResetPatch(): Pick<Task, "taskDoneRetryCount" | "workflowStepRetries" | "stuckKillCount"> {
|
||||||
|
return {
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
|
stuckKillCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -342,6 +342,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
baseBranch: null,
|
baseBranch: null,
|
||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
});
|
});
|
||||||
@@ -380,6 +382,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
baseBranch: null,
|
baseBranch: null,
|
||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
});
|
});
|
||||||
@@ -406,6 +410,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
baseBranch: null,
|
baseBranch: null,
|
||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
});
|
});
|
||||||
@@ -435,6 +441,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
status: null,
|
status: null,
|
||||||
error: null,
|
error: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
});
|
});
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
||||||
expect(store.logEntry).toHaveBeenCalledWith(
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
@@ -466,6 +474,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
status: null,
|
status: null,
|
||||||
error: null,
|
error: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
});
|
});
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
||||||
expect(store.logEntry).toHaveBeenCalledWith(
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
@@ -529,6 +539,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
status: null,
|
status: null,
|
||||||
error: null,
|
error: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
});
|
});
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
||||||
expect(store.logEntry).toHaveBeenCalledWith(
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
@@ -562,6 +574,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
status: null,
|
status: null,
|
||||||
error: null,
|
error: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
mergeRetries: 0,
|
mergeRetries: 0,
|
||||||
});
|
});
|
||||||
expect(store.moveTask).not.toHaveBeenCalled();
|
expect(store.moveTask).not.toHaveBeenCalled();
|
||||||
@@ -593,6 +607,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
status: null,
|
status: null,
|
||||||
error: null,
|
error: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
mergeRetries: 0,
|
mergeRetries: 0,
|
||||||
});
|
});
|
||||||
expect(store.moveTask).not.toHaveBeenCalled();
|
expect(store.moveTask).not.toHaveBeenCalled();
|
||||||
@@ -673,6 +689,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
baseBranch: null,
|
baseBranch: null,
|
||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
stuckKillCount: 0,
|
stuckKillCount: 0,
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
workflowStepRetries: 0,
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
});
|
});
|
||||||
@@ -1088,7 +1106,7 @@ describe("POST /tasks/:id/reset", () => {
|
|||||||
checkoutLeaseRenewedAt: null,
|
checkoutLeaseRenewedAt: null,
|
||||||
checkoutLeaseEpoch: null,
|
checkoutLeaseEpoch: null,
|
||||||
executionStartedAt: null,
|
executionStartedAt: null,
|
||||||
taskDoneRetryCount: null,
|
taskDoneRetryCount: 0,
|
||||||
worktreeSessionRetryCount: null,
|
worktreeSessionRetryCount: null,
|
||||||
sessionFile: null,
|
sessionFile: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
findDuplicateMatches,
|
findDuplicateMatches,
|
||||||
deterministicGuardLocks,
|
deterministicGuardLocks,
|
||||||
runDeterministicDuplicateGuard,
|
runDeterministicDuplicateGuard,
|
||||||
|
buildManualRetryResetPatch,
|
||||||
reconcileDeterministicDuplicate,
|
reconcileDeterministicDuplicate,
|
||||||
extractIntentSignature,
|
extractIntentSignature,
|
||||||
findNearDuplicates,
|
findNearDuplicates,
|
||||||
@@ -900,7 +901,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
await scopedStore.updateTask(req.params.id, {
|
await scopedStore.updateTask(req.params.id, {
|
||||||
status: null,
|
status: null,
|
||||||
error: null,
|
error: null,
|
||||||
stuckKillCount: 0,
|
...buildManualRetryResetPatch(),
|
||||||
});
|
});
|
||||||
await scopedStore.logEntry(
|
await scopedStore.logEntry(
|
||||||
req.params.id,
|
req.params.id,
|
||||||
@@ -914,7 +915,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
await scopedStore.updateTask(req.params.id, {
|
await scopedStore.updateTask(req.params.id, {
|
||||||
status: null,
|
status: null,
|
||||||
error: null,
|
error: null,
|
||||||
stuckKillCount: 0,
|
...buildManualRetryResetPatch(),
|
||||||
mergeRetries: 0,
|
mergeRetries: 0,
|
||||||
});
|
});
|
||||||
await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (in-review merge retry, mergeRetries reset)");
|
await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (in-review merge retry, mergeRetries reset)");
|
||||||
@@ -930,9 +931,9 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
branch: null,
|
branch: null,
|
||||||
baseBranch: null,
|
baseBranch: null,
|
||||||
baseCommitSha: null,
|
baseCommitSha: null,
|
||||||
stuckKillCount: 0,
|
|
||||||
recoveryRetryCount: null,
|
recoveryRetryCount: null,
|
||||||
nextRecoveryAt: null,
|
nextRecoveryAt: null,
|
||||||
|
...buildManualRetryResetPatch(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (retrySpecification) {
|
if (retrySpecification) {
|
||||||
|
|||||||
Reference in New Issue
Block a user