fix: clear stale failed state on executor resume
This commit is contained in:
@@ -2446,6 +2446,70 @@ describe("TaskExecutor pause behavior", () => {
|
|||||||
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Resuming execution after unpause");
|
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Resuming execution after unpause");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears stale failed state before resuming unpaused in-progress task", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
|
||||||
|
mockedCreateHaiAgent.mockImplementation(async () => ({
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
},
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test");
|
||||||
|
|
||||||
|
store._trigger("task:updated", {
|
||||||
|
id: "FN-001",
|
||||||
|
paused: undefined,
|
||||||
|
column: "in-progress",
|
||||||
|
status: "failed",
|
||||||
|
error: "Request was aborted.",
|
||||||
|
description: "Test task",
|
||||||
|
title: "Resumed task",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 30));
|
||||||
|
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: null, error: null });
|
||||||
|
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Resuming execution after unpause");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears stale failed state before resuming orphaned in-progress task", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
store.listTasks.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "FN-001",
|
||||||
|
column: "in-progress",
|
||||||
|
paused: false,
|
||||||
|
status: "failed",
|
||||||
|
error: "Request was aborted.",
|
||||||
|
title: "Active task",
|
||||||
|
steps: [],
|
||||||
|
description: "",
|
||||||
|
dependencies: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockedCreateHaiAgent.mockResolvedValue({
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test");
|
||||||
|
await executor.resumeOrphaned();
|
||||||
|
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: null, error: null });
|
||||||
|
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Resumed after engine restart");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not duplicate execution when unpausing already-executing task", async () => {
|
it("does not duplicate execution when unpausing already-executing task", async () => {
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
const disposeFn = vi.fn();
|
const disposeFn = vi.fn();
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ export class TaskExecutor {
|
|||||||
if (!this.executing.has(task.id)) {
|
if (!this.executing.has(task.id)) {
|
||||||
executorLog.log(`Unpaused ${task.id} in-progress with no session — resuming execution`);
|
executorLog.log(`Unpaused ${task.id} in-progress with no session — resuming execution`);
|
||||||
try {
|
try {
|
||||||
|
await this.clearResumeFailureState(task);
|
||||||
await this.store.logEntry(task.id, "Resuming execution after unpause");
|
await this.store.logEntry(task.id, "Resuming execution after unpause");
|
||||||
} catch { /* non-critical */ }
|
} catch { /* non-critical */ }
|
||||||
this.execute(task).catch((err) =>
|
this.execute(task).catch((err) =>
|
||||||
@@ -395,6 +396,12 @@ export class TaskExecutor {
|
|||||||
return task.steps.every((s) => s.status === "done" || s.status === "skipped");
|
return task.steps.every((s) => s.status === "done" || s.status === "skipped");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async clearResumeFailureState(task: Task): Promise<void> {
|
||||||
|
if (task.status === "failed" || task.error) {
|
||||||
|
await this.store.updateTask(task.id, { status: null, error: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fast-path a completed task directly to in-review without spawning a new agent.
|
* Fast-path a completed task directly to in-review without spawning a new agent.
|
||||||
* Captures modified files, runs workflow steps, and transitions the task.
|
* Captures modified files, runs workflow steps, and transitions the task.
|
||||||
@@ -461,6 +468,7 @@ export class TaskExecutor {
|
|||||||
|
|
||||||
executorLog.log(`Resuming ${task.id}: ${task.title || task.description.slice(0, 60)}`);
|
executorLog.log(`Resuming ${task.id}: ${task.title || task.description.slice(0, 60)}`);
|
||||||
try {
|
try {
|
||||||
|
await this.clearResumeFailureState(task);
|
||||||
await this.store.logEntry(task.id, "Resumed after engine restart");
|
await this.store.logEntry(task.id, "Resumed after engine restart");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
executorLog.error(`Failed to write resume log for ${task.id}:`, err);
|
executorLog.error(`Failed to write resume log for ${task.id}:`, err);
|
||||||
|
|||||||
Reference in New Issue
Block a user