diff --git a/CHANGELOG.md b/CHANGELOG.md index 13056320aee..3d0f4fe7a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Docker/GCP onboarding: reduce first-build OOM risk by capping Node heap during `pnpm install`, reuse existing gateway token during `docker-setup.sh` reruns so `.env` stays aligned with config, auto-bootstrap Control UI allowed origins for non-loopback Docker binds, and add GCP docs guidance for tokenized dashboard links + pairing recovery commands. (#26253) Thanks @pandego. - Pairing/Multi-account isolation: keep non-default account pairing allowlists and pending requests strictly account-scoped, while default account continues to use channel-scoped pairing allowlist storage. Thanks @gumadeiras. - Security/Config includes: harden `$include` file loading with verified-open reads, reject hardlinked include aliases, and enforce include file-size guardrails so config include resolution remains bounded to trusted in-root files. This ships in the next npm release (`2026.2.26`). Thanks @zpbrent for reporting. +- Cron/CLI: add `--session` for session-key routing and rename target selection to `--session-target` for `openclaw cron add/edit`, including `--clear-session` on edit for unsetting the key. (#27167) thanks @Matt-Hulme. ## 2026.2.25 diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 8d140192607..7abe6df8638 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -38,7 +38,7 @@ Create a one-shot reminder, verify it exists, and run it immediately: openclaw cron add \ --name "Reminder" \ --at "2026-02-01T16:00:00Z" \ - --session main \ + --session-target main \ --system-event "Reminder: check the cron docs draft" \ --wake now \ --delete-after-run @@ -55,7 +55,7 @@ openclaw cron add \ --name "Morning brief" \ --cron "0 7 * * *" \ --tz "America/Los_Angeles" \ - --session isolated \ + --session-target isolated \ --message "Summarize overnight updates." \ --announce \ --channel slack \ @@ -479,7 +479,7 @@ One-shot reminder (UTC ISO, auto-delete after success): openclaw cron add \ --name "Send reminder" \ --at "2026-01-12T18:00:00Z" \ - --session main \ + --session-target main \ --system-event "Reminder: submit expense report." \ --wake now \ --delete-after-run @@ -491,7 +491,7 @@ One-shot reminder (main session, wake immediately): openclaw cron add \ --name "Calendar check" \ --at "20m" \ - --session main \ + --session-target main \ --system-event "Next heartbeat: check calendar." \ --wake now ``` @@ -503,7 +503,7 @@ openclaw cron add \ --name "Morning status" \ --cron "0 7 * * *" \ --tz "America/Los_Angeles" \ - --session isolated \ + --session-target isolated \ --message "Summarize inbox + calendar for today." \ --announce \ --channel whatsapp \ @@ -518,7 +518,7 @@ openclaw cron add \ --cron "0 * * * * *" \ --tz "UTC" \ --stagger 30s \ - --session isolated \ + --session-target isolated \ --message "Run minute watcher checks." \ --announce ``` @@ -530,7 +530,7 @@ openclaw cron add \ --name "Nightly summary (topic)" \ --cron "0 22 * * *" \ --tz "America/Los_Angeles" \ - --session isolated \ + --session-target isolated \ --message "Summarize today; send to the nightly topic." \ --announce \ --channel telegram \ @@ -544,7 +544,7 @@ openclaw cron add \ --name "Deep analysis" \ --cron "0 6 * * 1" \ --tz "America/Los_Angeles" \ - --session isolated \ + --session-target isolated \ --message "Weekly deep analysis of project progress." \ --model "opus" \ --thinking high \ @@ -557,7 +557,7 @@ Agent selection (multi-agent setups): ```bash # Pin a job to agent "ops" (falls back to default if that agent is missing) -openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops +openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session-target isolated --message "Check ops queue" --agent ops # Switch or clear the agent on an existing job openclaw cron edit --agent ops diff --git a/docs/automation/cron-vs-heartbeat.md b/docs/automation/cron-vs-heartbeat.md index 9676d960d23..3124b72b1b1 100644 --- a/docs/automation/cron-vs-heartbeat.md +++ b/docs/automation/cron-vs-heartbeat.md @@ -106,7 +106,7 @@ openclaw cron add \ --name "Morning briefing" \ --cron "0 7 * * *" \ --tz "America/New_York" \ - --session isolated \ + --session-target isolated \ --message "Generate today's briefing: weather, calendar, top emails, news summary." \ --model opus \ --announce \ @@ -122,7 +122,7 @@ This runs at exactly 7:00 AM New York time, uses Opus for quality, and announces openclaw cron add \ --name "Meeting reminder" \ --at "20m" \ - --session main \ + --session-target main \ --system-event "Reminder: standup meeting starts in 10 minutes." \ --wake now \ --delete-after-run @@ -178,13 +178,13 @@ The most efficient setup uses **both**: ```bash # Daily morning briefing at 7am -openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --announce +openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session-target isolated --message "..." --announce # Weekly project review on Mondays at 9am -openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus +openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session-target isolated --message "..." --model opus # One-shot reminder -openclaw cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now +openclaw cron add --name "Call back" --at "2h" --session-target main --system-event "Call back the client" --wake now ``` ## Lobster: Deterministic workflows with approvals @@ -229,7 +229,7 @@ Both heartbeat and cron can interact with the main session, but differently: ### When to use main session cron -Use `--session main` with `--system-event` when you want: +Use `--session-target main` with `--system-event` when you want: - The reminder/event to appear in main session context - The agent to handle it during the next heartbeat with full context @@ -239,14 +239,14 @@ Use `--session main` with `--system-event` when you want: openclaw cron add \ --name "Check project" \ --every "4h" \ - --session main \ + --session-target main \ --system-event "Time for a project health check" \ --wake now ``` ### When to use isolated cron -Use `--session isolated` when you want: +Use `--session-target isolated` when you want: - A clean slate without prior context - Different model or thinking settings @@ -257,7 +257,7 @@ Use `--session isolated` when you want: openclaw cron add \ --name "Deep analysis" \ --cron "0 6 * * 0" \ - --session isolated \ + --session-target isolated \ --message "Weekly codebase analysis..." \ --model opus \ --thinking high \ diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index 940fbdad075..4085f1bf62a 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -42,6 +42,7 @@ type CronUpdatePatch = { schedule?: { kind?: string; expr?: string; tz?: string; staggerMs?: number }; payload?: { message?: string; model?: string; thinking?: string }; delivery?: { mode?: string; channel?: string; to?: string; bestEffort?: boolean }; + sessionKey?: unknown; }; }; @@ -51,6 +52,7 @@ type CronAddParams = { delivery?: { mode?: string }; deleteAfterRun?: boolean; agentId?: string; + sessionKey?: string; sessionTarget?: string; }; @@ -153,7 +155,7 @@ describe("cron cli", () => { "Daily", "--cron", "* * * * *", - "--session", + "--session-target", "isolated", "--message", "hello", @@ -180,7 +182,7 @@ describe("cron cli", () => { "Daily", "--cron", "* * * * *", - "--session", + "--session-target", "isolated", "--message", "hello", @@ -192,7 +194,7 @@ describe("cron cli", () => { expect(params?.delivery?.mode).toBe("announce"); }); - it("infers sessionTarget from payload when --session is omitted", async () => { + it("infers sessionTarget from payload when --session-target is omitted", async () => { await runCronCommand([ "cron", "add", @@ -234,7 +236,7 @@ describe("cron cli", () => { "Keep me", "--at", "20m", - "--session", + "--session-target", "main", "--system-event", "hello", @@ -262,7 +264,7 @@ describe("cron cli", () => { "Agent pinned", "--cron", "* * * * *", - "--session", + "--session-target", "isolated", "--message", "hi", @@ -275,6 +277,27 @@ describe("cron cli", () => { expect(params?.agentId).toBe("ops"); }); + it("sends session key on cron add", async () => { + await runCronCommand([ + "cron", + "add", + "--name", + "Session pinned", + "--cron", + "* * * * *", + "--session-target", + "isolated", + "--message", + "hi", + "--session", + " agent:project-lead:calculator ", + ]); + + const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add"); + const params = addCall?.[2] as { sessionKey?: string }; + expect(params?.sessionKey).toBe("agent:project-lead:calculator"); + }); + it.each([ { label: "omits empty model and thinking", @@ -305,6 +328,28 @@ describe("cron cli", () => { expect(clearPatch?.patch?.agentId).toBeNull(); }); + it("sets and clears session key on cron edit", async () => { + await runCronCommand(["cron", "edit", "job-1", "--session", " agent:project-lead:dashboard "]); + + const setPatch = getGatewayCallParams<{ patch?: { sessionKey?: unknown } }>("cron.update"); + expect(setPatch?.patch?.sessionKey).toBe("agent:project-lead:dashboard"); + + await runCronCommand(["cron", "edit", "job-2", "--clear-session"]); + const clearPatch = getGatewayCallParams<{ patch?: { sessionKey?: unknown } }>("cron.update"); + expect(clearPatch?.patch?.sessionKey).toBeNull(); + }); + + it("rejects --session with --clear-session on cron edit", async () => { + await expectCronCommandExit([ + "cron", + "edit", + "job-1", + "--session", + "agent:project-lead:dashboard", + "--clear-session", + ]); + }); + it("allows model/thinking updates without --message", async () => { await runCronCommand(["cron", "edit", "job-1", "--model", "opus", "--thinking", "low"]); @@ -418,7 +463,7 @@ describe("cron cli", () => { "0 * * * *", "--stagger", "45s", - "--session", + "--session-target", "main", "--system-event", "tick", @@ -434,7 +479,7 @@ describe("cron cli", () => { "--cron", "0 * * * *", "--exact", - "--session", + "--session-target", "main", "--system-event", "tick", @@ -454,7 +499,7 @@ describe("cron cli", () => { "--stagger", "1m", "--exact", - "--session", + "--session-target", "main", "--system-event", "tick", @@ -471,7 +516,7 @@ describe("cron cli", () => { "10m", "--stagger", "30s", - "--session", + "--session-target", "main", "--system-event", "tick", diff --git a/src/cli/cron-cli/register.cron-add.ts b/src/cli/cron-cli/register.cron-add.ts index 8d44c77778f..415875c8ba9 100644 --- a/src/cli/cron-cli/register.cron-add.ts +++ b/src/cli/cron-cli/register.cron-add.ts @@ -70,8 +70,8 @@ export function registerCronAddCommand(cron: Command) { .option("--delete-after-run", "Delete one-shot job after it succeeds", false) .option("--keep-after-run", "Keep one-shot job after it succeeds", false) .option("--agent ", "Agent id for this job") - .option("--session ", "Session target (main|isolated)") - .option("--session-key ", "Session key for job routing (e.g. agent:my-agent:my-session)") + .option("--session-target ", "Session target (main|isolated)") + .option("--session ", "Session key for job routing (e.g. agent:my-agent:my-session)") .option("--wake ", "Wake mode (now|next-heartbeat)", "now") .option("--at ", "Run once at time (ISO) or +duration (e.g. 20m)") .option("--every ", "Run every duration (e.g. 10m, 1h)") @@ -195,13 +195,14 @@ export function registerCronAddCommand(cron: Command) { typeof cmd?.getOptionValueSource === "function" ? (name: string) => cmd.getOptionValueSource(name) : () => undefined; - const sessionSource = optionSource("session"); - const sessionTargetRaw = typeof opts.session === "string" ? opts.session.trim() : ""; + const sessionSource = optionSource("sessionTarget"); + const sessionTargetRaw = + typeof opts.sessionTarget === "string" ? opts.sessionTarget.trim() : ""; const inferredSessionTarget = payload.kind === "agentTurn" ? "isolated" : "main"; const sessionTarget = sessionSource === "cli" ? sessionTargetRaw || "" : inferredSessionTarget; if (sessionTarget !== "main" && sessionTarget !== "isolated") { - throw new Error("--session must be main or isolated"); + throw new Error("--session-target must be main or isolated"); } if (opts.deleteAfterRun && opts.keepAfterRun) { @@ -218,7 +219,7 @@ export function registerCronAddCommand(cron: Command) { (opts.announce || typeof opts.deliver === "boolean") && (sessionTarget !== "isolated" || payload.kind !== "agentTurn") ) { - throw new Error("--announce/--no-deliver require --session isolated."); + throw new Error("--announce/--no-deliver require --session-target isolated."); } const deliveryMode = @@ -242,8 +243,8 @@ export function registerCronAddCommand(cron: Command) { : undefined; const sessionKey = - typeof opts.sessionKey === "string" && opts.sessionKey.trim() - ? opts.sessionKey.trim() + typeof opts.session === "string" && opts.session.trim() + ? opts.session.trim() : undefined; const params = { diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index 9bc9916a06d..6c45110e428 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -34,11 +34,11 @@ export function registerCronEditCommand(cron: Command) { .option("--disable", "Disable job", false) .option("--delete-after-run", "Delete one-shot job after it succeeds", false) .option("--keep-after-run", "Keep one-shot job after it succeeds", false) - .option("--session ", "Session target (main|isolated)") + .option("--session-target ", "Session target (main|isolated)") .option("--agent ", "Set agent id") .option("--clear-agent", "Unset agent and use default", false) - .option("--session-key ", "Set session key for job routing") - .option("--clear-session-key", "Unset session key", false) + .option("--session ", "Set session key for job routing") + .option("--clear-session", "Unset session key", false) .option("--wake ", "Wake mode (now|next-heartbeat)") .option("--at ", "Set one-shot time (ISO) or duration like 20m") .option("--every ", "Set interval duration like 10m") @@ -63,14 +63,14 @@ export function registerCronEditCommand(cron: Command) { .option("--no-best-effort-deliver", "Fail job when delivery fails") .action(async (id, opts) => { try { - if (opts.session === "main" && opts.message) { + if (opts.sessionTarget === "main" && opts.message) { throw new Error( - "Main jobs cannot use --message; use --system-event or --session isolated.", + "Main jobs cannot use --message; use --system-event or --session-target isolated.", ); } - if (opts.session === "isolated" && opts.systemEvent) { + if (opts.sessionTarget === "isolated" && opts.systemEvent) { throw new Error( - "Isolated jobs cannot use --system-event; use --message or --session main.", + "Isolated jobs cannot use --system-event; use --message or --session-target main.", ); } if (opts.announce && typeof opts.deliver === "boolean") { @@ -120,8 +120,8 @@ export function registerCronEditCommand(cron: Command) { if (opts.keepAfterRun) { patch.deleteAfterRun = false; } - if (typeof opts.session === "string") { - patch.sessionTarget = opts.session; + if (typeof opts.sessionTarget === "string") { + patch.sessionTarget = opts.sessionTarget; } if (typeof opts.wake === "string") { patch.wakeMode = opts.wake; @@ -135,13 +135,13 @@ export function registerCronEditCommand(cron: Command) { if (opts.clearAgent) { patch.agentId = null; } - if (opts.sessionKey && opts.clearSessionKey) { - throw new Error("Use --session-key or --clear-session-key, not both"); + if (opts.session && opts.clearSession) { + throw new Error("Use --session or --clear-session, not both"); } - if (typeof opts.sessionKey === "string" && opts.sessionKey.trim()) { - patch.sessionKey = opts.sessionKey.trim(); + if (typeof opts.session === "string" && opts.session.trim()) { + patch.sessionKey = opts.session.trim(); } - if (opts.clearSessionKey) { + if (opts.clearSession) { patch.sessionKey = null; }