From a8a4993ffd1b7e38f76c464c3c8c2f903af09c57 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 11 Jan 2026 10:52:40 +0000 Subject: [PATCH] fix: trim cron model overrides and doc guidance (#711) (thanks @mjrussell) --- CHANGELOG.md | 1 + docs/automation/cron-jobs.md | 4 ++ src/cli/cron-cli.test.ts | 110 +++++++++++++++++++++++++++++++++++ src/cli/cron-cli.ts | 13 ++++- 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/cli/cron-cli.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c59840b66..be2853fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changes - macOS: prompt to install the global `clawdbot` CLI when missing in local mode; install via `clawd.bot/install-cli.sh` (no onboarding) and use external launchd/CLI instead of the embedded gateway runtime. - Docs: add gog calendar event color IDs from `gog calendar colors`. (#715) — thanks @mjrussell. +- Cron/CLI: trim model overrides on cron edits and document main-session guidance. (#711) — thanks @mjrussell. ## 2026.1.10 diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 69cd4b526..055d320a1 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -69,6 +69,10 @@ Isolated jobs (`agentTurn`) can override the model and thinking level: - `model`: Provider/model string (e.g., `anthropic/claude-sonnet-4-20250514`) or alias (e.g., `opus`) - `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`) +Note: You can set `model` on main-session jobs too, but it changes the shared main +session model. We recommend model overrides only for isolated jobs to avoid +unexpected context shifts. + Resolution priority: 1. Job payload override (highest) 2. Hook-specific defaults (e.g., `hooks.gmail.model`) diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts new file mode 100644 index 000000000..eed40041b --- /dev/null +++ b/src/cli/cron-cli.test.ts @@ -0,0 +1,110 @@ +import { Command } from "commander"; +import { describe, expect, it, vi } from "vitest"; + +const callGatewayFromCli = vi.fn( + async (method: string, _opts: unknown, params?: unknown) => { + if (method === "cron.status") return { enabled: true }; + return { ok: true, params }; + }, +); + +vi.mock("./gateway-rpc.js", async () => { + const actual = + await vi.importActual( + "./gateway-rpc.js", + ); + return { + ...actual, + callGatewayFromCli: ( + method: string, + opts: unknown, + params?: unknown, + extra?: unknown, + ) => callGatewayFromCli(method, opts, params, extra), + }; +}); + +vi.mock("../runtime.js", () => ({ + defaultRuntime: { + log: vi.fn(), + error: vi.fn(), + exit: (code: number) => { + throw new Error(`__exit__:${code}`); + }, + }, +})); + +describe("cron cli", () => { + it("trims model and thinking on cron add", async () => { + callGatewayFromCli.mockClear(); + + const { registerCronCli } = await import("./cron-cli.js"); + const program = new Command(); + program.exitOverride(); + registerCronCli(program); + + await program.parseAsync( + [ + "cron", + "add", + "--name", + "Daily", + "--cron", + "* * * * *", + "--session", + "isolated", + "--message", + "hello", + "--model", + " opus ", + "--thinking", + " low ", + ], + { from: "user" }, + ); + + const addCall = callGatewayFromCli.mock.calls.find( + (call) => call[0] === "cron.add", + ); + const params = addCall?.[2] as { + payload?: { model?: string; thinking?: string }; + }; + + expect(params?.payload?.model).toBe("opus"); + expect(params?.payload?.thinking).toBe("low"); + }); + + it("omits empty model and thinking on cron edit", 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", + "hello", + "--model", + " ", + "--thinking", + " ", + ], + { from: "user" }, + ); + + const updateCall = callGatewayFromCli.mock.calls.find( + (call) => call[0] === "cron.update", + ); + const patch = updateCall?.[2] as { + patch?: { payload?: { model?: string; thinking?: string } }; + }; + + expect(patch?.patch?.payload?.model).toBeUndefined(); + expect(patch?.patch?.payload?.thinking).toBeUndefined(); + }); +}); diff --git a/src/cli/cron-cli.ts b/src/cli/cron-cli.ts index 9c18c925e..62c2e6a17 100644 --- a/src/cli/cron-cli.ts +++ b/src/cli/cron-cli.ts @@ -646,15 +646,22 @@ export function registerCronCli(program: Command) { text: String(opts.systemEvent), }; } else if (opts.message) { + const model = + typeof opts.model === "string" && opts.model.trim() + ? opts.model.trim() + : undefined; + const thinking = + typeof opts.thinking === "string" && opts.thinking.trim() + ? opts.thinking.trim() + : undefined; const timeoutSeconds = opts.timeoutSeconds ? Number.parseInt(String(opts.timeoutSeconds), 10) : undefined; patch.payload = { kind: "agentTurn", message: String(opts.message), - model: typeof opts.model === "string" ? opts.model : undefined, - thinking: - typeof opts.thinking === "string" ? opts.thinking : undefined, + model, + thinking, timeoutSeconds: timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds