fix: clear stale failed state on executor resume

This commit is contained in:
gsxdsm
2026-04-05 17:59:29 -07:00
parent 161a8da290
commit 57a3d9009b
2 changed files with 72 additions and 0 deletions

View File

@@ -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();

View File

@@ -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);