fix(cron): use --session-target and --session flags (#27167) (thanks @Matt-Hulme)

This commit is contained in:
Peter Steinberger
2026-02-26 13:27:46 +01:00
parent 912b55ac18
commit 9d3394b459
6 changed files with 96 additions and 49 deletions

View File

@@ -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

View File

@@ -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 <jobId> --agent ops

View File

@@ -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 \

View File

@@ -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",

View File

@@ -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 <id>", "Agent id for this job")
.option("--session <target>", "Session target (main|isolated)")
.option("--session-key <key>", "Session key for job routing (e.g. agent:my-agent:my-session)")
.option("--session-target <target>", "Session target (main|isolated)")
.option("--session <key>", "Session key for job routing (e.g. agent:my-agent:my-session)")
.option("--wake <mode>", "Wake mode (now|next-heartbeat)", "now")
.option("--at <when>", "Run once at time (ISO) or +duration (e.g. 20m)")
.option("--every <duration>", "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 = {

View File

@@ -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 <target>", "Session target (main|isolated)")
.option("--session-target <target>", "Session target (main|isolated)")
.option("--agent <id>", "Set agent id")
.option("--clear-agent", "Unset agent and use default", false)
.option("--session-key <key>", "Set session key for job routing")
.option("--clear-session-key", "Unset session key", false)
.option("--session <key>", "Set session key for job routing")
.option("--clear-session", "Unset session key", false)
.option("--wake <mode>", "Wake mode (now|next-heartbeat)")
.option("--at <when>", "Set one-shot time (ISO) or duration like 20m")
.option("--every <duration>", "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;
}