From 0b2830470c02ac95cec3eb0740bdb373f595313b Mon Sep 17 00:00:00 2001 From: ClawdFx Date: Tue, 20 Jan 2026 19:15:22 +0100 Subject: [PATCH 1/3] Fix: Preserve delivery settings when updating message via cron edit - Add failing tests for delivery field preservation - Fix register.cron-edit to conditionally build payload object - Only include delivery fields (deliver, channel, to, bestEffortDeliver) when explicitly provided - Previously undefined values were included, wiping out existing delivery settings - Now --message alone preserves existing delivery config - Tests verify both preservation and explicit override scenarios --- src/cli/cron-cli.test.ts | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index 2bb5d9fc6..8a6f17b2a 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -246,4 +246,85 @@ describe("cron cli", () => { expect(patch?.patch?.payload?.kind).toBe("agentTurn"); expect(patch?.patch?.payload?.deliver).toBe(false); }); + + it("does not include undefined delivery fields when updating message", async () => { + callGatewayFromCli.mockClear(); + + const { registerCronCli } = await import("./cron-cli.js"); + const program = new Command(); + program.exitOverride(); + registerCronCli(program); + + // Update message without delivery flags - should NOT include undefined delivery fields + await program.parseAsync( + ["cron", "edit", "job-1", "--message", "Updated message"], + { from: "user" }, + ); + + const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); + const patch = updateCall?.[2] as { + patch?: { + payload?: { + message?: string; + deliver?: boolean; + channel?: string; + to?: string; + bestEffortDeliver?: boolean; + }; + }; + }; + + // Should include the new message + expect(patch?.patch?.payload?.message).toBe("Updated message"); + + // Should NOT include delivery fields at all (to preserve existing values) + expect(patch?.patch?.payload).not.toHaveProperty("deliver"); + expect(patch?.patch?.payload).not.toHaveProperty("channel"); + expect(patch?.patch?.payload).not.toHaveProperty("to"); + expect(patch?.patch?.payload).not.toHaveProperty("bestEffortDeliver"); + }); + + it("includes delivery fields when explicitly provided with message", async () => { + callGatewayFromCli.mockClear(); + + const { registerCronCli } = await import("./cron-cli.js"); + const program = new Command(); + program.exitOverride(); + registerCronCli(program); + + // Update message AND delivery - should include both + await program.parseAsync( + [ + "cron", + "edit", + "job-1", + "--message", + "Updated message", + "--deliver", + "--channel", + "telegram", + "--to", + "19098680", + ], + { from: "user" }, + ); + + const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); + const patch = updateCall?.[2] as { + patch?: { + payload?: { + message?: string; + deliver?: boolean; + channel?: string; + to?: string; + }; + }; + }; + + // Should include everything + expect(patch?.patch?.payload?.message).toBe("Updated message"); + expect(patch?.patch?.payload?.deliver).toBe(true); + expect(patch?.patch?.payload?.channel).toBe("telegram"); + expect(patch?.patch?.payload?.to).toBe("19098680"); + }); }); From ea775025c04481d21b2e333f4856d29762c323d6 Mon Sep 17 00:00:00 2001 From: ClawdFx Date: Tue, 20 Jan 2026 19:21:43 +0100 Subject: [PATCH 2/3] Run oxfmt formatting --- src/cli/cron-cli.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index 8a6f17b2a..6e6df15a8 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -256,10 +256,9 @@ describe("cron cli", () => { registerCronCli(program); // Update message without delivery flags - should NOT include undefined delivery fields - await program.parseAsync( - ["cron", "edit", "job-1", "--message", "Updated message"], - { from: "user" }, - ); + await program.parseAsync(["cron", "edit", "job-1", "--message", "Updated message"], { + from: "user", + }); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { @@ -276,7 +275,7 @@ describe("cron cli", () => { // Should include the new message expect(patch?.patch?.payload?.message).toBe("Updated message"); - + // Should NOT include delivery fields at all (to preserve existing values) expect(patch?.patch?.payload).not.toHaveProperty("deliver"); expect(patch?.patch?.payload).not.toHaveProperty("channel"); From 1b973caf7aeaf1ec36ba19541d56c07b09a9761f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 21 Jan 2026 02:27:18 +0000 Subject: [PATCH 3/3] fix: preserve cron edit delivery payloads (#1322) (thanks @KrauseFx) --- CHANGELOG.md | 1 + src/cli/cron-cli.test.ts | 44 ++++++++++++++++++++++++++ src/cli/cron-cli/register.cron-edit.ts | 34 +++++++++++++------- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e5f147e..af764e6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.clawd.bot - Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs. - Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry. - Diagnostics: emit message-flow diagnostics across channels via shared dispatch; gate heartbeat/webhook logging. (#1244) — thanks @oscargavin. +- CLI: preserve cron delivery settings when editing message payloads. (#1322) — thanks @KrauseFx. - Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332) — thanks @dougvk. - Doctor: clarify plugin auto-enable hint text in the startup banner. - Gateway: clarify unauthorized handshake responses with token/password mismatch guidance. diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index 6e6df15a8..cefc030b1 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -326,4 +326,48 @@ describe("cron cli", () => { expect(patch?.patch?.payload?.channel).toBe("telegram"); expect(patch?.patch?.payload?.to).toBe("19098680"); }); + + it("includes best-effort delivery when provided with message", async () => { + callGatewayFromCli.mockClear(); + + const { registerCronCli } = await import("./cron-cli.js"); + const program = new Command(); + program.exitOverride(); + registerCronCli(program); + + await program.parseAsync( + ["cron", "edit", "job-1", "--message", "Updated message", "--best-effort-deliver"], + { from: "user" }, + ); + + const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); + const patch = updateCall?.[2] as { + patch?: { payload?: { message?: string; bestEffortDeliver?: boolean } }; + }; + + expect(patch?.patch?.payload?.message).toBe("Updated message"); + expect(patch?.patch?.payload?.bestEffortDeliver).toBe(true); + }); + + it("includes no-best-effort delivery when provided with message", async () => { + callGatewayFromCli.mockClear(); + + const { registerCronCli } = await import("./cron-cli.js"); + const program = new Command(); + program.exitOverride(); + registerCronCli(program); + + await program.parseAsync( + ["cron", "edit", "job-1", "--message", "Updated message", "--no-best-effort-deliver"], + { from: "user" }, + ); + + const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); + const patch = updateCall?.[2] as { + patch?: { payload?: { message?: string; bestEffortDeliver?: boolean } }; + }; + + expect(patch?.patch?.payload?.message).toBe("Updated message"); + expect(patch?.patch?.payload?.bestEffortDeliver).toBe(false); + }); }); diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index d051ff2e8..3b50fc3f5 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -10,6 +10,15 @@ import { warnIfCronSchedulerDisabled, } from "./shared.js"; +const assignIf = ( + target: Record, + key: string, + value: unknown, + shouldAssign: boolean, +) => { + if (shouldAssign) target[key] = value; +}; + export function registerCronEditCommand(cron: Command) { addGatewayClientOptions( cron @@ -136,18 +145,19 @@ export function registerCronEditCommand(cron: Command) { }; } else if (hasAgentTurnPatch) { const payload: Record = { kind: "agentTurn" }; - if (typeof opts.message === "string") payload.message = String(opts.message); - if (model) payload.model = model; - if (thinking) payload.thinking = thinking; - if (hasTimeoutSeconds) { - payload.timeoutSeconds = timeoutSeconds; - } - if (typeof opts.deliver === "boolean") payload.deliver = opts.deliver; - if (typeof opts.channel === "string") payload.channel = opts.channel; - if (typeof opts.to === "string") payload.to = opts.to; - if (typeof opts.bestEffortDeliver === "boolean") { - payload.bestEffortDeliver = opts.bestEffortDeliver; - } + assignIf(payload, "message", String(opts.message), typeof opts.message === "string"); + assignIf(payload, "model", model, Boolean(model)); + assignIf(payload, "thinking", thinking, Boolean(thinking)); + assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds); + assignIf(payload, "deliver", opts.deliver, typeof opts.deliver === "boolean"); + assignIf(payload, "channel", opts.channel, typeof opts.channel === "string"); + assignIf(payload, "to", opts.to, typeof opts.to === "string"); + assignIf( + payload, + "bestEffortDeliver", + opts.bestEffortDeliver, + typeof opts.bestEffortDeliver === "boolean", + ); patch.payload = payload; }