Merge pull request #65 from bluk1020/fix/stuck-detector-workflow-timeout-fallback
fix: keep stuck detector active by default
This commit is contained in:
5
.changeset/stuck-detector-workflow-timeout.md
Normal file
5
.changeset/stuck-detector-workflow-timeout.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Keep stuck task detection active by default with an explicit task-stuck timeout default, without coupling it to workflow step timeout settings.
|
||||||
@@ -69,6 +69,11 @@ describe("settings key parity", () => {
|
|||||||
expect(DEFAULT_PROJECT_SETTINGS.completionDocumentationMode).toBe("off");
|
expect(DEFAULT_PROJECT_SETTINGS.completionDocumentationMode).toBe("off");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps task stuck timeout active by default without coupling to workflow step timeout", () => {
|
||||||
|
expect(DEFAULT_PROJECT_SETTINGS.taskStuckTimeoutMs).toBe(600_000);
|
||||||
|
expect(DEFAULT_PROJECT_SETTINGS.workflowStepTimeoutMs).toBe(360_000);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps github tracking keys in expected scopes with documented defaults", () => {
|
it("keeps github tracking keys in expected scopes with documented defaults", () => {
|
||||||
expect(DEFAULT_PROJECT_SETTINGS.githubTrackingEnabledByDefault).toBe(false);
|
expect(DEFAULT_PROJECT_SETTINGS.githubTrackingEnabledByDefault).toBe(false);
|
||||||
expect(DEFAULT_PROJECT_SETTINGS.githubTrackingDefaultRepo).toBeUndefined();
|
expect(DEFAULT_PROJECT_SETTINGS.githubTrackingDefaultRepo).toBeUndefined();
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
|||||||
requirePlanApproval: false,
|
requirePlanApproval: false,
|
||||||
specStalenessEnabled: false,
|
specStalenessEnabled: false,
|
||||||
specStalenessMaxAgeMs: 6 * 60 * 60 * 1000,
|
specStalenessMaxAgeMs: 6 * 60 * 60 * 1000,
|
||||||
taskStuckTimeoutMs: undefined,
|
taskStuckTimeoutMs: 600_000,
|
||||||
aiSessionTtlMs: 7 * 24 * 60 * 60 * 1000,
|
aiSessionTtlMs: 7 * 24 * 60 * 60 * 1000,
|
||||||
aiSessionCleanupIntervalMs: 60 * 60 * 1000,
|
aiSessionCleanupIntervalMs: 60 * 60 * 1000,
|
||||||
autoUnpauseEnabled: true,
|
autoUnpauseEnabled: true,
|
||||||
|
|||||||
@@ -2180,7 +2180,7 @@ export interface ProjectSettings {
|
|||||||
/** Timeout in milliseconds for detecting stuck tasks. When a task's agent session
|
/** Timeout in milliseconds for detecting stuck tasks. When a task's agent session
|
||||||
* shows no activity (no text deltas, tool calls, or progress updates) for longer
|
* shows no activity (no text deltas, tool calls, or progress updates) for longer
|
||||||
* than this duration, the task is considered stuck and will be terminated and retried.
|
* than this duration, the task is considered stuck and will be terminated and retried.
|
||||||
* Default: undefined (disabled). Suggested value: 600000 (10 minutes). */
|
* Default: 600000 (10 minutes). Set to 0 to disable. */
|
||||||
taskStuckTimeoutMs?: number;
|
taskStuckTimeoutMs?: number;
|
||||||
/** TTL in milliseconds for persisted AI planning/subtask/mission interview sessions.
|
/** TTL in milliseconds for persisted AI planning/subtask/mission interview sessions.
|
||||||
* Sessions older than this cutoff are expired by the dashboard session cleanup loop.
|
* Sessions older than this cutoff are expired by the dashboard session cleanup loop.
|
||||||
|
|||||||
@@ -718,21 +718,52 @@ describe("StuckTaskDetector", () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing when timeout is disabled", async () => {
|
it("does not couple stuck detection to workflow step timeout", async () => {
|
||||||
store = createMockStore({
|
store = createMockStore({
|
||||||
getSettings: vi.fn().mockResolvedValue({ taskStuckTimeoutMs: undefined }),
|
getSettings: vi.fn().mockResolvedValue({
|
||||||
|
taskStuckTimeoutMs: undefined,
|
||||||
|
workflowStepTimeoutMs: 60_000,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const customDetector = new StuckTaskDetector(store);
|
const onStuck = vi.fn();
|
||||||
|
const customDetector = new StuckTaskDetector(store, { onStuck });
|
||||||
const session = createMockSession();
|
const session = createMockSession();
|
||||||
|
|
||||||
customDetector.trackTask("FN-001", session);
|
customDetector.trackTask("FN-001", session);
|
||||||
|
|
||||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||||
vi.advanceTimersByTime(61000);
|
vi.advanceTimersByTime(61_000);
|
||||||
|
|
||||||
await customDetector.checkNow();
|
await customDetector.checkNow();
|
||||||
|
|
||||||
expect(store.moveTask).not.toHaveBeenCalled();
|
expect(onStuck).not.toHaveBeenCalled();
|
||||||
|
expect(session.dispose).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kills stuck sessions when the project default stuck timeout is present", async () => {
|
||||||
|
store = createMockStore({
|
||||||
|
getSettings: vi.fn().mockResolvedValue({
|
||||||
|
taskStuckTimeoutMs: 600_000,
|
||||||
|
workflowStepTimeoutMs: 60_000,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const onStuck = vi.fn();
|
||||||
|
const customDetector = new StuckTaskDetector(store, { onStuck });
|
||||||
|
const session = createMockSession();
|
||||||
|
|
||||||
|
customDetector.trackTask("FN-001", session);
|
||||||
|
|
||||||
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||||
|
vi.advanceTimersByTime(601_000);
|
||||||
|
|
||||||
|
await customDetector.checkNow();
|
||||||
|
|
||||||
|
expect(onStuck).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ taskId: "FN-001", reason: "inactivity" }),
|
||||||
|
);
|
||||||
|
expect(session.dispose).toHaveBeenCalled();
|
||||||
|
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
* - `recordProgress(taskId)` — step transitions (in-progress, done, skipped); resets counters
|
* - `recordProgress(taskId)` — step transitions (in-progress, done, skipped); resets counters
|
||||||
*
|
*
|
||||||
* The detector polls at a configurable interval and compares timestamps against
|
* The detector polls at a configurable interval and compares timestamps against
|
||||||
* `taskStuckTimeoutMs` from settings.
|
* `taskStuckTimeoutMs` from settings. Project defaults keep this active by
|
||||||
|
* default while keeping workflow-step execution timeouts independent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TaskStore, Settings } from "@fusion/core";
|
import type { TaskStore, Settings } from "@fusion/core";
|
||||||
@@ -471,7 +472,7 @@ export class StuckTaskDetector {
|
|||||||
if (settings.globalPause || settings.enginePaused) return;
|
if (settings.globalPause || settings.enginePaused) return;
|
||||||
|
|
||||||
const timeoutMs = settings.taskStuckTimeoutMs;
|
const timeoutMs = settings.taskStuckTimeoutMs;
|
||||||
if (!timeoutMs || timeoutMs <= 0) return; // Disabled
|
if (!timeoutMs || timeoutMs <= 0) return; // Disabled when task stuck timeout is explicitly unset/disabled
|
||||||
|
|
||||||
const stuckTasks: string[] = [];
|
const stuckTasks: string[] = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user