From e15d5d0533d678a029f6b349834ace0034958760 Mon Sep 17 00:00:00 2001 From: Shadow Date: Mon, 12 Jan 2026 22:16:07 -0600 Subject: [PATCH] Cron: persist enabled=false patches Closes #205 --- CHANGELOG.md | 1 + src/cron/normalize.ts | 11 ++++++++++ src/gateway/server.cron.test.ts | 39 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0e7b601..a31fd6193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm) ### Fixes +- Cron: coerce enabled patches so disabling jobs persists correctly. (#205 — thanks @thewilloftheshadow) - Control UI: keep chat scroll position unless user is near the bottom. (#217 — thanks @thewilloftheshadow) - Fallback: treat credential validation failures ("no credentials found", "no API key found") as auth errors that trigger model fallback. (#822 — thanks @sebslight) - Telegram: preserve forum topic thread ids, including General topic replies. (#727 — thanks @thewilloftheshadow) diff --git a/src/cron/normalize.ts b/src/cron/normalize.ts index 07c4611b8..3fb7af490 100644 --- a/src/cron/normalize.ts +++ b/src/cron/normalize.ts @@ -65,6 +65,17 @@ export function normalizeCronJobInput( } } + if ("enabled" in base) { + const enabled = (base as UnknownRecord).enabled; + if (typeof enabled === "boolean") { + next.enabled = enabled; + } else if (typeof enabled === "string") { + const trimmed = enabled.trim().toLowerCase(); + if (trimmed === "true") next.enabled = true; + if (trimmed === "false") next.enabled = false; + } + } + if (isRecord(base.schedule)) { next.schedule = coerceSchedule(base.schedule); } diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index 4347bde07..f63c9387d 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -221,6 +221,45 @@ describe("gateway server cron", () => { testState.cronStorePath = undefined; }); + test("disables cron jobs via enabled:false patches", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-cron-")); + testState.cronStorePath = path.join(dir, "cron", "jobs.json"); + await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true }); + await fs.writeFile( + testState.cronStorePath, + JSON.stringify({ version: 1, jobs: [] }), + ); + + const { server, ws } = await startServerWithClient(); + await connectOk(ws); + + const addRes = await rpcReq(ws, "cron.add", { + name: "disable test", + enabled: true, + schedule: { kind: "every", everyMs: 60_000 }, + sessionTarget: "main", + wakeMode: "next-heartbeat", + payload: { kind: "systemEvent", text: "hello" }, + }); + expect(addRes.ok).toBe(true); + const jobIdValue = (addRes.payload as { id?: unknown } | null)?.id; + const jobId = typeof jobIdValue === "string" ? jobIdValue : ""; + expect(jobId.length > 0).toBe(true); + + const updateRes = await rpcReq(ws, "cron.update", { + id: jobId, + patch: { enabled: false }, + }); + expect(updateRes.ok).toBe(true); + const updated = updateRes.payload as { enabled?: unknown } | undefined; + expect(updated?.enabled).toBe(false); + + ws.close(); + await server.close(); + await fs.rm(dir, { recursive: true, force: true }); + testState.cronStorePath = undefined; + }); + test("writes cron run history to runs/.jsonl", async () => { const dir = await fs.mkdtemp( path.join(os.tmpdir(), "clawdbot-gw-cron-log-"),