From 32986f6b5087c5d6716cda32671cb13a2ad61533 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 21:51:28 -0700 Subject: [PATCH] fix(core): tolerate missing done task mirrors on startup --- .../core/src/__tests__/store-create.test.ts | 13 +++++++++++++ packages/core/src/store.ts | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/core/src/__tests__/store-create.test.ts b/packages/core/src/__tests__/store-create.test.ts index 1186dea920..b55ab812c1 100644 --- a/packages/core/src/__tests__/store-create.test.ts +++ b/packages/core/src/__tests__/store-create.test.ts @@ -67,6 +67,19 @@ describe("TaskStore", () => { }); }); + describe("startup watch recovery", () => { + it("does not crash done-task backfill when a DB row has no task.json mirror", async () => { + const task = await store.createTask({ description: "done task with missing mirror" }); + (store as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db + .prepare(`UPDATE tasks SET "column" = ?, updatedAt = ? WHERE id = ?`) + .run("done", new Date().toISOString(), task.id); + await deleteTaskDir(task.id); + + await expect(store.watch()).resolves.toBeUndefined(); + await store.close(); + }); + }); + describe("breakIntoSubtasks task creation flag", () => { it("persists breakIntoSubtasks=true when explicitly requested", async () => { const task = await store.createTask({ diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 32307168d3..1d21bda1ae 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -11712,7 +11712,24 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} if (cachedTask.column !== "done") continue; const taskDir = this.taskDir(taskId); - const raw = await readFile(join(taskDir, "task.json"), "utf-8"); + let raw: string; + try { + raw = await readFile(join(taskDir, "task.json"), "utf-8"); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { + /* + * FNXC:StartupRecovery 2026-06-23-05:02: + * A recovered or corrupt SQLite index can retain done-task rows whose legacy task.json mirror was already removed. Startup watch must not crash while running the one-time done-pause backfill; skip the missing mirror and keep the dashboard available so operators can inspect or repair the project. + */ + storeLog.warn("Skipping done-task pause metadata backfill for missing task.json", { + phase: "watch:done-pause-backfill", + taskId, + taskJsonPath: join(taskDir, "task.json"), + }); + continue; + } + throw error; + } const diskTask = JSON.parse(raw) as Task; if (!this.clearDoneTransientFields(diskTask)) continue;