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");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const store = createMockStore();
|
||||
const disposeFn = vi.fn();
|
||||
|
||||
@@ -320,6 +320,7 @@ export class TaskExecutor {
|
||||
if (!this.executing.has(task.id)) {
|
||||
executorLog.log(`Unpaused ${task.id} in-progress with no session — resuming execution`);
|
||||
try {
|
||||
await this.clearResumeFailureState(task);
|
||||
await this.store.logEntry(task.id, "Resuming execution after unpause");
|
||||
} catch { /* non-critical */ }
|
||||
this.execute(task).catch((err) =>
|
||||
@@ -395,6 +396,12 @@ export class TaskExecutor {
|
||||
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.
|
||||
* 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)}`);
|
||||
try {
|
||||
await this.clearResumeFailureState(task);
|
||||
await this.store.logEntry(task.id, "Resumed after engine restart");
|
||||
} catch (err) {
|
||||
executorLog.error(`Failed to write resume log for ${task.id}:`, err);
|
||||
|
||||
Reference in New Issue
Block a user