From da0689c6e11ee5b4f06c3b7019fe0957db3c702f Mon Sep 17 00:00:00 2001 From: liuedcson <332840681+liuedcson@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:04:13 +0800 Subject: [PATCH] fix(coding-agent): record a cron task's status reason when it is created `CronCreateOptions` declares `statusReason`, and the persisted-record reader validates it, but `create()` never wrote it. Only `update()` did. A host that imports a task whose bound session no longer exists creates it already paused, with `statusReason: "session_missing"`, so the reason was dropped on the one path that needed it. The task still showed as paused, which is why the symptom was a missing explanation rather than a wrong status; the type promised a field the write did not store. The new test creates such a task, reloads the store, and asserts both the status and the reason survive. --- .../coding-agent/src/features/step-cron.ts | 4 +++ packages/coding-agent/test/step-cron.test.ts | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/coding-agent/src/features/step-cron.ts b/packages/coding-agent/src/features/step-cron.ts index b549fedf..5f7bfc36 100644 --- a/packages/coding-agent/src/features/step-cron.ts +++ b/packages/coding-agent/src/features/step-cron.ts @@ -707,6 +707,10 @@ export class StepCronRuntime { ...(options.sessionCreated ? { sessionCreated: true } : {}), ...(options.title ? { title: options.title.slice(0, CRON_MAX_TITLE_LENGTH) } : {}), ...(options.status ? { status: options.status } : {}), + // `CronStatusReason` is part of the create options, not only the update + // patch: a host that imports a task whose bound session is already gone + // creates it paused, and the reason has to survive that same write. + ...(options.statusReason ? { statusReason: options.statusReason } : {}), ...(options.inputs ? { inputs: { skills: [...(options.inputs.skills ?? [])] } } : {}), ...(options.model ? { model: { ...options.model } } : {}), ...(options.permissionPreset ? { permissionPreset: options.permissionPreset } : {}), diff --git a/packages/coding-agent/test/step-cron.test.ts b/packages/coding-agent/test/step-cron.test.ts index bc6bd81c..52c48d35 100644 --- a/packages/coding-agent/test/step-cron.test.ts +++ b/packages/coding-agent/test/step-cron.test.ts @@ -825,6 +825,32 @@ test("an unschedulable persisted recurrence does not block other jobs on restart }); describe("desktop cron task fields", () => { + test("a task created paused for a missing session keeps its reason", () => { + // The record reader validates `statusReason`, so a host that creates a task + // already paused (an imported task whose session is gone) must have the + // reason written on creation, not only through a later update. + const now = Date.UTC(2026, 0, 1, 12, 0); + const directory = makeDirectory(); + const file = path.join(directory, "tasks.json"); + const first = new StepCronRuntime({ now: () => now, jitterRatio: 0, storagePath: file }); + const created = first.create("0 9 * * *", "orphaned task", true, true, true, { + id: "orphan", + sessionId: "session-gone", + status: "paused", + statusReason: "session_missing", + origin: "desktop", + autoExpireMs: null, + jitter: false, + }).job; + expect(created.statusReason).toBe("session_missing"); + + const reloaded = new StepCronRuntime({ now: () => now, jitterRatio: 0, storagePath: file }); + reloaded.start(directory, true); + const record = reloaded.list().find((job) => job.id === "orphan"); + expect(record?.statusReason).toBe("session_missing"); + expect(record?.status).toBe("paused"); + }); + test("creates a v2 task with task session metadata and updates it", () => { const now = Date.UTC(2026, 0, 1, 12, 0); const directory = makeDirectory();