fix(FN-000): clear stale queued state on task resume
Resume paths (unpause, drift recovery, engine restart) bypassed the scheduler's todo->in-progress clear, leaving actively executing tasks labeled status="queued" with a lingering blockedBy. Broadened clearResumeFailureState to null both fields alongside the existing failure cleanup, and added a defensive UI backstop so the "Queued" badge no longer renders for tasks in the in-progress column. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1062,7 +1062,7 @@ function TaskCardComponent({
|
||||
<Layers size={12} style={{ verticalAlign: "middle" }} /> {task.blockedBy}
|
||||
</span>
|
||||
)}
|
||||
{(queued || task.status === "queued") && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: "middle" }} /> Queued</span>}
|
||||
{(queued || task.status === "queued") && task.column !== "in-progress" && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: "middle" }} /> Queued</span>}
|
||||
</div>
|
||||
)}
|
||||
<PluginSlot slotId="task-card-badge" projectId={projectId} />
|
||||
|
||||
@@ -568,8 +568,8 @@ describe("TaskCard queued badge logic", () => {
|
||||
}
|
||||
|
||||
/** Mirrors the queued-badge visibility condition from TaskCard.tsx */
|
||||
function shouldShowQueuedBadge(opts: { queued?: boolean; status?: string | null }): boolean {
|
||||
return !!(opts.queued || opts.status === "queued");
|
||||
function shouldShowQueuedBadge(opts: { queued?: boolean; status?: string | null; column?: string }): boolean {
|
||||
return !!(opts.queued || opts.status === "queued") && opts.column !== "in-progress";
|
||||
}
|
||||
|
||||
it("shows queued-badge when queued prop is true", () => {
|
||||
@@ -589,6 +589,11 @@ describe("TaskCard queued badge logic", () => {
|
||||
expect(shouldShowQueuedBadge({})).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT show queued-badge when column is 'in-progress' even if status is stale 'queued'", () => {
|
||||
expect(shouldShowQueuedBadge({ status: "queued", column: "in-progress" })).toBe(false);
|
||||
expect(shouldShowQueuedBadge({ queued: true, column: "in-progress" })).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT show card-status-badge when status is 'queued'", () => {
|
||||
expect(shouldShowStatusBadge("queued")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -745,8 +745,25 @@ export class TaskExecutor {
|
||||
}
|
||||
|
||||
private async clearResumeFailureState(task: Task): Promise<void> {
|
||||
const updates: { status?: null; error?: null; blockedBy?: null } = {};
|
||||
if (task.status === "failed" || task.error) {
|
||||
await this.store.updateTask(task.id, { status: null, error: null });
|
||||
updates.status = null;
|
||||
updates.error = null;
|
||||
}
|
||||
// Pre-dispatch gating state must not survive into a resumed in-progress run.
|
||||
// The scheduler sets status="queued" + blockedBy on dep/file-scope conflicts
|
||||
// (scheduler.ts:618, 660) and clears them on the todo→in-progress transition
|
||||
// (scheduler.ts:696). Resume paths (unpause, drift recovery, engine restart)
|
||||
// bypass that clear, so a task can end up actively executing while still
|
||||
// labeled "queued" in the UI.
|
||||
if (task.status === "queued") {
|
||||
updates.status = null;
|
||||
}
|
||||
if (task.blockedBy) {
|
||||
updates.blockedBy = null;
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await this.store.updateTask(task.id, updates);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user