mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-21 12:04:59 +00:00
fix(cron): use --session-target and --session flags (#27167) (thanks @Matt-Hulme)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user