fix: stabilize workflow board and loop recovery
This commit is contained in:
@@ -1154,6 +1154,40 @@ describe("StuckTaskDetector", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("suppresses another loop classification while accepted recovery is pending", async () => {
|
||||
const onLoopDetected = vi.fn().mockResolvedValue(true);
|
||||
const customDetector = new StuckTaskDetector(store, { onLoopDetected });
|
||||
const session = createMockSession();
|
||||
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
customDetector.trackTask("FN-201", session);
|
||||
vi.advanceTimersByTime(61000);
|
||||
for (let i = 0; i < 80; i++) {
|
||||
customDetector.recordActivity("FN-201");
|
||||
}
|
||||
|
||||
expect(customDetector.classifyStuckReason("FN-201", 60000)).toBe("loop");
|
||||
await customDetector.killAndRetry("FN-201", 60000);
|
||||
|
||||
expect(customDetector.classifyStuckReason("FN-201", 60000)).toBeNull();
|
||||
|
||||
customDetector.markLoopObserved("FN-201");
|
||||
for (let i = 0; i < 25; i++) {
|
||||
customDetector.recordIgnoredStepUpdate("FN-201");
|
||||
}
|
||||
expect(customDetector.classifyStuckReason("FN-201", 60000)).toBe("no-progress-churn");
|
||||
|
||||
customDetector.recordProgress("FN-201");
|
||||
vi.advanceTimersByTime(61000);
|
||||
for (let i = 0; i < 80; i++) {
|
||||
customDetector.recordActivity("FN-201");
|
||||
}
|
||||
|
||||
expect(customDetector.classifyStuckReason("FN-201", 60000)).toBe("loop");
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("does NOT call onLoopDetected when reason is inactivity", async () => {
|
||||
const onLoopDetected = vi.fn().mockResolvedValue(true);
|
||||
const onStuck = vi.fn();
|
||||
|
||||
@@ -163,6 +163,25 @@ describe("WorkflowGraphTaskRunner (CU-U2)", () => {
|
||||
expect(result.reason).toMatch(/workflow-missing/);
|
||||
});
|
||||
|
||||
it("resolves built-in workflow selections without requiring the store to return a definition", async () => {
|
||||
const calls: string[] = [];
|
||||
const store: WorkflowGraphRunnerStore = {
|
||||
getTaskWorkflowSelection: () => ({ workflowId: "builtin:coding", stepIds: [] }),
|
||||
getWorkflowDefinition: async () => undefined,
|
||||
};
|
||||
const runner = new WorkflowGraphTaskRunner({
|
||||
store,
|
||||
seams: recordingSeams(calls),
|
||||
runCustomNode: async () => ({ outcome: "success" }),
|
||||
});
|
||||
|
||||
const result = await runner.run(task, flagOn);
|
||||
|
||||
expect(result.disposition).toBe("completed");
|
||||
expect(calls).toEqual(["execute", "review", "merge"]);
|
||||
expect(result.reason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back (never strands the task) when the interpreter throws", async () => {
|
||||
// Malformed graph: edge references unknown node → WorkflowIrError inside run().
|
||||
const badIr: WorkflowIr = {
|
||||
|
||||
@@ -419,6 +419,15 @@ export class StuckTaskDetector {
|
||||
return "no-progress-churn";
|
||||
}
|
||||
|
||||
// After onLoopDetected accepts compact-and-resume, the executor owns the
|
||||
// recovery prompt. Do not immediately classify the same stale timestamps as
|
||||
// another loop before the executor has a chance to emit fresh progress. The
|
||||
// deterministic no-progress-churn terminal path above still has to fire if
|
||||
// the recovered session keeps hammering rejected step updates.
|
||||
if (entry.recoveryInProgress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check loop — active but not making progress, with enough activity to be a real loop
|
||||
if (noProgressMs >= timeoutMs && entry.activitySinceProgress >= LOOP_ACTIVITY_THRESHOLD) {
|
||||
return "loop";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Settings, TaskDetail, WorkflowDefinition } from "@fusion/core";
|
||||
import { isExperimentalFeatureEnabled } from "@fusion/core";
|
||||
import { getBuiltinWorkflow, isBuiltinWorkflowId, isExperimentalFeatureEnabled } from "@fusion/core";
|
||||
|
||||
import { WorkflowGraphExecutor, type WorkflowNodeOutcome } from "./workflow-graph-executor.js";
|
||||
import type {
|
||||
@@ -146,7 +146,9 @@ export class WorkflowGraphTaskRunner {
|
||||
|
||||
let definition: WorkflowDefinition | undefined;
|
||||
try {
|
||||
definition = await this.deps.store.getWorkflowDefinition(selection.workflowId);
|
||||
definition = isBuiltinWorkflowId(selection.workflowId)
|
||||
? getBuiltinWorkflow(selection.workflowId)
|
||||
: await this.deps.store.getWorkflowDefinition(selection.workflowId);
|
||||
} catch (err) {
|
||||
return this.fallBack(task.id, `workflow-load-error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user