From 6f9da38401de104cc81f6057ce5dce711f218b9d Mon Sep 17 00:00:00 2001 From: Omar Shahine Date: Thu, 30 Jul 2026 08:54:55 -0700 Subject: [PATCH] feat(agents): rename model- and user-facing scheduler strings to automations (#114852) * fix(agents): teach canonical automations tool in fallback guidance and reuse the identity source Review follow-ups: the structured-list fallback still taught models the cron tool; the cron-scope test echoed its own stub; MCP serve allowlist and voice confirmation hardcoded the name instead of the canonical constant. * fix(mcp): place automations identity import outside the header comment * feat(agents): rename model- and user-facing scheduler strings to automations Rewrites every string the model or user sees that names the feature: tool label and terminal presentation, display-summary preset, subagents catalog description, heartbeat guidance, subagent prompt, session labels (Cron: -> Automation:), default job name, unattended-run preamble, delivery awareness text, task-ledger progress summary, failure alerts, auto-disable notification, model preflight diagnostics, validation errors, and the shipped workspace AGENTS.md template. Adds a system-prompt backstop rule on the automations tool line: the feature is called automations, never cron. Cron stays only where it names the schedule syntax ({kind:"cron"}, cron expressions), config keys (cron.triggers.enabled), RPC method names, and session-key namespaces. Test fixtures with legacy labels are kept as stored-data coverage. Part of RFC openclaw/rfcs#50 Phase 1 (PR B). * docs(templates): keep workspace template on the live docs anchor until the docs PR renames it * fix(cron): rename remaining failure-notification producers and stale test expectations Codex review follow-ups: the per-run failure notification (server-cron- notifications) and the doctor legacy-notify advisory still said Cron job; failure-alert and incomplete-turn suites still asserted the old wording. * chore(plugin-sdk): re-emit plugin-state test runtime dts CI's extension-boundary runner holds stale tsgo incremental state whose cached plugin-state-test-runtime.d.ts predates the keyed-store re-exports; the freshness stamp validates it and reruns reuse the same sticky disk. Touching the module forces incremental emit to regenerate the declaration. Fresh builds of this head (local exact CI command and a forced Testbox rebuild) both emit the exports correctly. * fix(agents): rename residual model- and operator-facing cron prose Found in combined dev-gateway E2E: fallback tool line still told models the scheduler tool is cron; heartbeat scratch description, cron model preflight rejection, doctor and Claw prose still said cron jobs. * fix(agents): finish model-facing scheduler strings flagged in review Loop work-order prompts, subagents tool description, cron-tool write errors, and the headless exec denial label now say automations; loop builders covered by prompt assertions; snapshots regenerated. * chore(agents): refresh generated baselines and live docs anchor after rebase onto main * fix(agents): rename new main-side cron tool error expectations to automations * fix(gateway): rename main-side cron notification test expectations to automations * chore(agents): apply review moves - automations commands in workspace template, drop unrelated sdk comment --------- Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com> --- .../.generated/plugin-sdk-api-baseline.sha256 | 2 +- docs/reference/templates/AGENTS.md | 6 ++-- src/agents/bash-tools.exec-host-shared.ts | 2 +- .../bash-tools.exec.approval-id.test.ts | 2 +- .../run.incomplete-turn.test.ts | 4 +-- src/agents/heartbeat-system-prompt.test.ts | 2 +- src/agents/subagent-system-prompt.ts | 2 +- src/agents/system-prompt.ts | 4 +-- src/agents/tool-catalog.ts | 2 +- src/agents/tool-description-presets.ts | 2 +- src/agents/tools/cron-tool-write.ts | 4 +-- .../tools/cron-tool.flat-params.test.ts | 12 +++---- src/agents/tools/cron-tool.pacing.test.ts | 2 +- src/agents/tools/cron-tool.test.ts | 32 +++++++++---------- src/agents/tools/cron-tool.ts | 24 +++++++------- src/agents/tools/heartbeat-response-tool.ts | 2 +- src/agents/tools/subagents-tool.test.ts | 4 ++- src/agents/tools/subagents-tool.ts | 2 +- src/auto-reply/heartbeat.ts | 2 +- src/auto-reply/reply/commands-loop.test.ts | 6 ++++ src/auto-reply/reply/commands-loop.ts | 11 ++++--- src/claws/cron.ts | 2 +- src/claws/doctor.ts | 2 +- .../doctor-heartbeat-cadence-migration.ts | 2 +- .../doctor-heartbeat-task-migration.ts | 2 +- src/commands/doctor-sandbox.ts | 2 +- src/commands/doctor/cron/legacy-notify.ts | 2 +- .../isolated-agent.model-formatting.test.ts | 8 ++--- .../delivery-dispatch-awareness.ts | 4 +-- .../delivery-dispatch.double-announce.test.ts | 26 +++++++-------- .../isolated-agent/model-preflight.runtime.ts | 4 +-- src/cron/isolated-agent/model-selection.ts | 4 +-- .../isolated-agent/run-prepare-runtime.ts | 2 +- src/cron/isolated-agent/run-prepare.ts | 2 +- .../run.cron-model-override.test.ts | 4 +-- .../run.message-tool-policy.test.ts | 4 +-- .../isolated-agent/run.skill-filter.test.ts | 6 ++-- src/cron/service.failure-alert.test.ts | 24 +++++++------- src/cron/service/failure-alerts.ts | 2 +- src/cron/service/jobs-scheduling.ts | 2 +- src/cron/service/normalize.ts | 6 ++-- src/cron/service/ops.test.ts | 8 ++--- src/cron/service/task-ledger.ts | 4 +-- src/cron/service/timer.test.ts | 4 +-- src/gateway/server-cron-notifications.test.ts | 6 ++-- src/gateway/server-cron-notifications.ts | 2 +- src/gateway/server.cron.test.ts | 8 ++--- src/gateway/server/hooks.agent-trust.test.ts | 2 +- .../codex-dynamic-tools.discord-group.json | 4 +-- .../codex-dynamic-tools.heartbeat-turn.json | 6 ++-- .../codex-dynamic-tools.telegram-direct.json | 4 +-- .../discord-group-codex-message-tool.md | 8 ++--- .../telegram-direct-codex-message-tool.md | 8 ++--- .../telegram-heartbeat-codex-tool.md | 18 +++++------ 54 files changed, 167 insertions(+), 154 deletions(-) diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 8517f202d532..770da779b665 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -107,7 +107,7 @@ aa2a56b4448c8ebdec9d06aac95d809995f533093d42fa32cd75e1d852967245 module/questio 7994045066b29af1fc6b36ae32068f2a6f277195971af84701cb739cc23d0579 module/reply-dispatch-runtime ac2b199e95c5c8b1e2a65e62bd41d1b6322e531bca294ef4979a297a12640bce module/reply-history f394fe4d5a7ed9e4d574063ae44e8d6af85c9a0e7d8b329f750ca16b0664325f module/reply-payload -78ebacfbc22a8129b9400e08c54e17722590eea9e9fa4ccb9c8f5651fe034345 module/reply-runtime +1f899eb54013f268d6698ce8e6943289ea0e872747e8e868259106829518db86 module/reply-runtime b4043b356372f6af64ee3c26e4d6a6d623b817e4d95346358dc0e64a3b61d1e0 module/root-walk 536341e301631a14ac67bd7e8d10d2ba770ff91c5f7a2c5dc8dd9dfd1c1c7ec4 module/routing ff6cca86f54f94f238205f5b122af36666314e0a380f3ec7f0ccb9ed9208df31 module/run-command diff --git a/docs/reference/templates/AGENTS.md b/docs/reference/templates/AGENTS.md index 3c2cba727c52..cb56642e4805 100644 --- a/docs/reference/templates/AGENTS.md +++ b/docs/reference/templates/AGENTS.md @@ -114,9 +114,9 @@ Example placeholders (replace or remove them): ## Heartbeats - Be Proactive -When you receive a heartbeat poll (message matches the configured heartbeat prompt), don't just reply `HEARTBEAT_OK` every time. Keep a short checklist or reminders in the heartbeat monitor's cron scratch; use `openclaw cron list --all` to find the monitor job, then `openclaw cron scratch --set "..."` to update it. Keep it small to limit token burn. +When you receive a heartbeat poll (message matches the configured heartbeat prompt), don't just reply `HEARTBEAT_OK` every time. Keep a short checklist or reminders in the heartbeat monitor's automation scratch; use `openclaw automations list --all` to find the monitor job, then `openclaw automations scratch --set "..."` to update it. Keep it small to limit token burn. -See [Scheduled Tasks (Cron) vs Heartbeat](/automation#automations-vs-heartbeat) for the full decision table. Short version: heartbeat batches periodic checks with full session context on approximate timing (default every 30 minutes); cron is for exact timing, isolated runs, a different model, or one-shot reminders. +See [Automations vs Heartbeat](/automation#automations-vs-heartbeat) for the full decision table. Short version: heartbeat batches periodic checks with full session context on approximate timing (default every 30 minutes); automations are for exact timing, isolated runs, a different model, or one-shot reminders. **Things to check (rotate through these, 2-4 times per day):** emails for urgent unread messages; calendar for events in the next 24-48h; social mentions; weather if your human might go out. @@ -151,5 +151,5 @@ This is a starting point. Add your own conventions, style, and rules as you figu ## Related - [Default AGENTS.md](/reference/AGENTS.default) -- [Scheduled tasks vs heartbeat](/automation#automations-vs-heartbeat) +- [Automations vs heartbeat](/automation#automations-vs-heartbeat) - [Heartbeat](/gateway/heartbeat) diff --git a/src/agents/bash-tools.exec-host-shared.ts b/src/agents/bash-tools.exec-host-shared.ts index 03c4a304a9bd..24e8c1384cea 100644 --- a/src/agents/bash-tools.exec-host-shared.ts +++ b/src/agents/bash-tools.exec-host-shared.ts @@ -432,7 +432,7 @@ export function buildHeadlessExecApprovalDeniedMessage(params: { ask: ExecAsk; askFallback: ExecApprovalsResolved["agent"]["askFallback"]; }): string { - const runLabel = params.trigger === "cron" ? "Cron runs" : "Headless runs"; + const runLabel = params.trigger === "cron" ? "Automation runs" : "Headless runs"; return [ `exec denied: ${runLabel} cannot wait for interactive exec approval.`, `Effective host exec policy: security=${params.security} ask=${params.ask} askFallback=${params.askFallback}`, diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index b2cbadb61600..2b055dd1cf45 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -1709,7 +1709,7 @@ describe("exec approvals", () => { tool.execute("call-cron-denied", { command: "echo cron-denied", }), - ).rejects.toThrow("Cron runs cannot wait for interactive exec approval"); + ).rejects.toThrow("Automation runs cannot wait for interactive exec approval"); }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts b/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts index b6897a3a3774..cef79ee1a828 100644 --- a/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts +++ b/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts @@ -525,7 +525,7 @@ describe("runEmbeddedAgent incomplete-turn safety", () => { toolName: "cron", argsHash: "args", resultHash: "result", - terminalPresentation: "Cron scheduler status.\nEnabled: yes", + terminalPresentation: "Automations scheduler status.\nEnabled: yes", }); return makeAttemptResult({ assistantTexts: [], @@ -554,7 +554,7 @@ describe("runEmbeddedAgent incomplete-turn safety", () => { expect(result.payloads).toEqual([ { text: - "Cron scheduler status.\nEnabled: yes\n\n" + + "Automations scheduler status.\nEnabled: yes\n\n" + "⚠️ Agent couldn't generate a response. Please try again.", isError: true, }, diff --git a/src/agents/heartbeat-system-prompt.test.ts b/src/agents/heartbeat-system-prompt.test.ts index bacdfd229237..d71952baa580 100644 --- a/src/agents/heartbeat-system-prompt.test.ts +++ b/src/agents/heartbeat-system-prompt.test.ts @@ -124,7 +124,7 @@ describe("resolveHeartbeatPromptForSystemPrompt", () => { agentId: "main", defaultAgentId: "main", }), - ).toContain("Recurring tasks are cron jobs"); + ).toContain("Recurring tasks are automations"); }); it("does not inject the heartbeat section for non-default agents", () => { diff --git a/src/agents/subagent-system-prompt.ts b/src/agents/subagent-system-prompt.ts index 79939483ac2c..cb6ceada877d 100644 --- a/src/agents/subagent-system-prompt.ts +++ b/src/agents/subagent-system-prompt.ts @@ -63,7 +63,7 @@ export function buildSubagentSystemPrompt(params: { "## What You DON'T Do", `- No user conversation or pretending to be ${parentLabel}.`, "- No external message unless explicitly tasked to message specific recipient/channel.", - "- No cron/persistent state.", + "- No automations/persistent state.", `- Report via plain final text, never \`message\`.`, "", ]; diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 7d3c682fa392..7e805abda4f4 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -88,7 +88,7 @@ const CONTEXT_FILE_ORDER = new Map([ const DYNAMIC_CONTEXT_FILE_BASENAMES = new Set(); const DEFAULT_HEARTBEAT_PROMPT_CONTEXT_BLOCK = - "Default heartbeat prompt:\n`Follow the heartbeat monitor scratch context when provided. Recurring tasks are cron jobs; create or change their schedules with cron tools or the openclaw cron CLI, not heartbeat scratch. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`"; + "Default heartbeat prompt:\n`Follow the heartbeat monitor scratch context when provided. Recurring tasks are automations; create or change their schedules with the automations tool, not heartbeat scratch. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`"; const SYSTEM_PROMPT_STABLE_PREFIX_CACHE_LIMIT = 64; type StablePromptPrefixCacheEntry = { @@ -865,7 +865,7 @@ export function buildAgentSystemPrompt(params: { canvas: "Present/eval/snapshot Canvas", nodes: "Paired node status/control/media", [AUTOMATIONS_TOOL_NAME]: - "Schedule/wake. Reminder text must read as reminder when fired; mention reminder for delayed gaps; include useful recent context.", + "Schedule/wake. Reminder text must read as reminder when fired; mention reminder for delayed gaps; include useful recent context. This feature is called automations; never call it cron.", message: "Message/channel actions", conversations_list: "List exact external conversation addresses", conversations_send: "Send directly to an external conversation", diff --git a/src/agents/tool-catalog.ts b/src/agents/tool-catalog.ts index 0ad58dfcc802..8f292e5fbcac 100644 --- a/src/agents/tool-catalog.ts +++ b/src/agents/tool-catalog.ts @@ -249,7 +249,7 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [ { id: "subagents", label: "subagents", - description: "Background work: subagents, media gen, cron runs. list/cancel.", + description: "Background work: subagents, media gen, automation runs. list/cancel.", sectionId: "sessions", profiles: ["coding", "messaging"], includeInOpenClawGroup: true, diff --git a/src/agents/tool-description-presets.ts b/src/agents/tool-description-presets.ts index 48b80bcffcee..c02f99b751ed 100644 --- a/src/agents/tool-description-presets.ts +++ b/src/agents/tool-description-presets.ts @@ -2,7 +2,7 @@ // descriptions when a longer contextual description is assembled elsewhere. export const EXEC_TOOL_DISPLAY_SUMMARY = "Run shell now."; export const PROCESS_TOOL_DISPLAY_SUMMARY = "Inspect/control exec sessions."; -export const CRON_TOOL_DISPLAY_SUMMARY = "Schedule reminders, cron, wake events."; +export const CRON_TOOL_DISPLAY_SUMMARY = "Schedule reminders, automations, wake events."; export const SESSIONS_LIST_TOOL_DISPLAY_SUMMARY = "List visible sessions; filters/previews."; export const SESSIONS_HISTORY_TOOL_DISPLAY_SUMMARY = "Read sanitized session history."; export const SESSIONS_SEARCH_TOOL_DISPLAY_SUMMARY = "Search past session transcripts."; diff --git a/src/agents/tools/cron-tool-write.ts b/src/agents/tools/cron-tool-write.ts index b6685289fbfa..a675de0cc00b 100644 --- a/src/agents/tools/cron-tool-write.ts +++ b/src/agents/tools/cron-tool-write.ts @@ -12,13 +12,13 @@ export function assertNoCronShellExecution(value: unknown): void { const payload = isRecord(value.payload) ? value.payload : undefined; if (normalizeLowercaseStringOrEmpty(payload?.kind) === "command") { throw new Error( - "cron command payloads cannot be created or edited through the agent cron tool; use the CLI or Gateway API.", + "automation command payloads cannot be created or edited through the agent automations tool; use the CLI or Gateway API.", ); } const schedule = isRecord(value.schedule) ? value.schedule : undefined; if (schedule?.kind === "on-exit") { throw new Error( - "cron on-exit schedules cannot be created or edited through the agent cron tool; use the CLI or Gateway API.", + "automation on-exit schedules cannot be created or edited through the agent automations tool; use the CLI or Gateway API.", ); } // Stream argv is authorized by the Gateway's cron.triggers.enabled gate, diff --git a/src/agents/tools/cron-tool.flat-params.test.ts b/src/agents/tools/cron-tool.flat-params.test.ts index f2d194b55f16..e615e3d4de4c 100644 --- a/src/agents/tools/cron-tool.flat-params.test.ts +++ b/src/agents/tools/cron-tool.flat-params.test.ts @@ -52,7 +52,7 @@ describe("cron tool flat-params", () => { }, }, ), - ).toEqual({ text: "Cron jobs listed.\nCount: 2" }); + ).toEqual({ text: "Automations listed.\nCount: 2" }); expect( terminalPresentation( { action: "list" }, @@ -64,7 +64,7 @@ describe("cron tool flat-params", () => { }, }, ), - ).toEqual({ text: "Cron jobs listed.\nCount: 250" }); + ).toEqual({ text: "Automations listed.\nCount: 250" }); expect( terminalPresentation( { action: "add" }, @@ -177,7 +177,7 @@ describe("cron tool flat-params", () => { cwd: "/repo", message: "rebuilt", }), - ).rejects.toThrow("cron on-exit schedules cannot be created or edited"); + ).rejects.toThrow("automation on-exit schedules cannot be created or edited"); expect(callGatewayToolMock).not.toHaveBeenCalled(); }); @@ -191,7 +191,7 @@ describe("cron tool flat-params", () => { command: "make", message: "done", }), - ).rejects.toThrow("cron on-exit schedules cannot be created or edited"); + ).rejects.toThrow("automation on-exit schedules cannot be created or edited"); expect(callGatewayToolMock).not.toHaveBeenCalled(); }); @@ -206,7 +206,7 @@ describe("cron tool flat-params", () => { command: "pnpm build", cwd: "/repo", }), - ).rejects.toThrow("cron on-exit schedules cannot be created or edited"); + ).rejects.toThrow("automation on-exit schedules cannot be created or edited"); expect(callGatewayToolMock).not.toHaveBeenCalled(); }); @@ -219,7 +219,7 @@ describe("cron tool flat-params", () => { jobId: "job-infer", command: "make", }), - ).rejects.toThrow("cron on-exit schedules cannot be created or edited"); + ).rejects.toThrow("automation on-exit schedules cannot be created or edited"); expect(callGatewayToolMock).not.toHaveBeenCalled(); }); diff --git a/src/agents/tools/cron-tool.pacing.test.ts b/src/agents/tools/cron-tool.pacing.test.ts index f76718136d52..43e55253d89a 100644 --- a/src/agents/tools/cron-tool.pacing.test.ts +++ b/src/agents/tools/cron-tool.pacing.test.ts @@ -71,7 +71,7 @@ describe("cron next_check action", () => { jobId: "another-job", in: "15m", }), - ).rejects.toThrow("Cron tool is restricted to the current cron job."); + ).rejects.toThrow("Automations tool is restricted to the current automation."); }); it("rejects next_check outside a current cron run", async () => { diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 3f24b633b137..eb2f1463ad40 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -220,7 +220,7 @@ describe("cron tool", () => { action: "remove", jobId: "job-other", }), - ).rejects.toThrow("Cron tool is restricted to the current cron job."); + ).rejects.toThrow("Automations tool is restricted to the current automation."); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -266,7 +266,7 @@ describe("cron tool", () => { }); await expect(tool.execute("call-runs-denied", args)).rejects.toThrow( - "Cron tool is restricted to the current cron job.", + "Automations tool is restricted to the current automation.", ); expect(callGatewayMock).not.toHaveBeenCalled(); @@ -328,7 +328,7 @@ describe("cron tool", () => { const tool = createTestCronTool({ selfRemoveOnlyJobId: "job-current" }); await expect(tool.execute("call-get-denied", args)).rejects.toThrow( - "Cron tool is restricted to the current cron job.", + "Automations tool is restricted to the current automation.", ); expect(callGatewayMock).not.toHaveBeenCalled(); @@ -494,7 +494,7 @@ describe("cron tool", () => { const tool = createTestCronTool({ selfRemoveOnlyJobId: "job-current" }); await expect(tool.execute("call-denied", args)).rejects.toThrow( - "Cron tool is restricted to the current cron job.", + "Automations tool is restricted to the current automation.", ); expect(callGatewayMock).not.toHaveBeenCalled(); @@ -694,7 +694,7 @@ describe("cron tool", () => { text: "follow up", sessionKey: "agent:agent-456:discord:thread-xyz", }), - ).rejects.toThrow("cron sessionKey must match the calling agent"); + ).rejects.toThrow("automations sessionKey must match the calling agent"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -1088,7 +1088,7 @@ describe("cron tool", () => { agentId: null, }, }), - ).rejects.toThrow("cron job agentId must match the calling agent"); + ).rejects.toThrow("automation agentId must match the calling agent"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -1161,7 +1161,7 @@ describe("cron tool", () => { agentId: "worker", }, }), - ).rejects.toThrow("cron job agentId must match the calling agent"); + ).rejects.toThrow("automation agentId must match the calling agent"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -1180,7 +1180,7 @@ describe("cron tool", () => { sessionTarget: "session:agent:worker:telegram:direct:alice", }, }), - ).rejects.toThrow("cron sessionTarget must match the calling agent"); + ).rejects.toThrow("automations sessionTarget must match the calling agent"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -1234,7 +1234,7 @@ describe("cron tool", () => { payload: { kind, argv: ["sh", "-lc", "echo ok"] }, }, }), - ).rejects.toThrow("cron command payloads cannot be created or edited"); + ).rejects.toThrow("automation command payloads cannot be created or edited"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -1278,7 +1278,7 @@ describe("cron tool", () => { payload: { kind: "agentTurn", message: "done" }, }, }), - ).rejects.toThrow("cron on-exit schedules cannot be created or edited"); + ).rejects.toThrow("automation on-exit schedules cannot be created or edited"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -1845,7 +1845,7 @@ describe("cron tool", () => { payload: { kind: "systemEvent", text: "Reminder: the thing." }, }, }), - ).rejects.toThrow("cron job agentId must match the calling agent"); + ).rejects.toThrow("automation agentId must match the calling agent"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -2403,7 +2403,7 @@ describe("cron tool", () => { id: "job-1", patch: { agentId: "worker" }, }), - ).rejects.toThrow("cron patch agentId cannot be changed"); + ).rejects.toThrow("automation patch agentId cannot be changed"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -2437,7 +2437,7 @@ describe("cron tool", () => { id: "job-1", patch: { sessionTarget: "session:agent:worker:telegram:direct:alice" }, }), - ).rejects.toThrow("cron sessionTarget must match the calling agent"); + ).rejects.toThrow("automations sessionTarget must match the calling agent"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -2496,7 +2496,7 @@ describe("cron tool", () => { payload: { kind, argv: ["sh", "-lc", "echo ok"] }, }, }), - ).rejects.toThrow("cron command payloads cannot be created or edited"); + ).rejects.toThrow("automation command payloads cannot be created or edited"); expect(callGatewayMock).not.toHaveBeenCalled(); }); @@ -2516,7 +2516,7 @@ describe("cron tool", () => { payload: { argv: ["sh", "-lc", "echo bypass"] }, }, }), - ).rejects.toThrow("cron command payloads cannot be created or edited"); + ).rejects.toThrow("automation command payloads cannot be created or edited"); expect(callGatewayMock).toHaveBeenCalledTimes(1); expect(readGatewayCall()).toEqual({ @@ -2556,7 +2556,7 @@ describe("cron tool", () => { schedule: { kind: "on-exit", command: "make" }, }, }), - ).rejects.toThrow("cron on-exit schedules cannot be created or edited"); + ).rejects.toThrow("automation on-exit schedules cannot be created or edited"); expect(callGatewayMock).not.toHaveBeenCalled(); }); diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 02e53af87034..c2e2362b14fa 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -142,15 +142,15 @@ function assertCronToolSessionRefsMatchScope( ): void { const sessionAgentId = readAgentIdFromCronToolSessionRef(value.sessionKey); if (sessionAgentId && normalizeAgentId(sessionAgentId) !== callerScope.agentId) { - throw new Error("cron sessionKey must match the calling agent"); + throw new Error("automations sessionKey must match the calling agent"); } const sessionTargetAgentId = readAgentIdFromCronToolSessionTarget(value.sessionTarget); if (sessionTargetAgentId && normalizeAgentId(sessionTargetAgentId) !== callerScope.agentId) { - throw new Error("cron sessionTarget must match the calling agent"); + throw new Error("automations sessionTarget must match the calling agent"); } } -const CRON_SELF_REMOVE_SCOPE_ERROR = "Cron tool is restricted to the current cron job."; +const CRON_SELF_REMOVE_SCOPE_ERROR = "Automations tool is restricted to the current automation."; function readCronSelfRemoveOnlyJobId(opts: CronToolOptions | undefined) { return opts?.selfRemoveOnlyJobId?.trim() || undefined; @@ -227,7 +227,7 @@ function formatCronTerminalPresentation( switch (params.action) { case "status": { const enabled = result.details.enabled === true ? "yes" : "no"; - return { text: `Cron scheduler status.\nEnabled: ${enabled}` }; + return { text: `Automations scheduler status.\nEnabled: ${enabled}` }; } case "list": { const total = @@ -239,18 +239,18 @@ function formatCronTerminalPresentation( const count = total ?? (Array.isArray(result.details.jobs) ? result.details.jobs.length : undefined); return count === undefined - ? { text: "Cron jobs listed." } - : { text: `Cron jobs listed.\nCount: ${count}` }; + ? { text: "Automations listed." } + : { text: `Automations listed.\nCount: ${count}` }; } case "get": - return { text: "Cron job loaded." }; + return { text: "Automation loaded." }; case "runs": { const entries = Array.isArray(result.details.entries) ? result.details.entries.length : undefined; return entries === undefined - ? { text: "Cron run history loaded." } - : { text: `Cron run history loaded.\nCount: ${entries}` }; + ? { text: "Automation run history loaded." } + : { text: `Automation run history loaded.\nCount: ${entries}` }; } default: return undefined; @@ -314,7 +314,7 @@ TRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.e DELIVERY {mode:"none"|"announce"|"webhook",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:"none". webhook posts finished-run event to URL in \`to\`. -Job wakeMode (main jobs): "now"(default)|"next-heartbeat". Restricted cron-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.`, +Job wakeMode (main jobs): "now"(default)|"next-heartbeat". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.`, parameters: createCronToolSchema(), execute: async (_toolCallId, args) => { const params = args as Record; @@ -476,7 +476,7 @@ Job wakeMode (main jobs): "now"(default)|"next-heartbeat". Restricted cron-run s if (callerScope) { assertCronToolAgentFieldMatchesScope({ value: (job as { agentId?: unknown }).agentId, - field: "cron job agentId", + field: "automation agentId", callerScope, }); (job as { agentId?: string }).agentId = callerScope.agentId; @@ -599,7 +599,7 @@ Job wakeMode (main jobs): "now"(default)|"next-heartbeat". Restricted cron-run s throw new Error("patch required"); } if (callerScope && "agentId" in patch) { - throw new Error("cron patch agentId cannot be changed by the agent cron tool"); + throw new Error("automation patch agentId cannot be changed by the automations tool"); } if (callerScope) { assertCronToolSessionRefsMatchScope(patch, callerScope); diff --git a/src/agents/tools/heartbeat-response-tool.ts b/src/agents/tools/heartbeat-response-tool.ts index b66661753604..a532a9d9b8a2 100644 --- a/src/agents/tools/heartbeat-response-tool.ts +++ b/src/agents/tools/heartbeat-response-tool.ts @@ -30,7 +30,7 @@ const HeartbeatResponseToolSchema = Type.Object( scratch: Type.Optional( Type.String({ description: - "Complete replacement for heartbeat monitor prose. Recurring schedules belong in cron jobs, not scratch.", + "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", }), ), }, diff --git a/src/agents/tools/subagents-tool.test.ts b/src/agents/tools/subagents-tool.test.ts index 27048928771d..d1f74a8a3872 100644 --- a/src/agents/tools/subagents-tool.test.ts +++ b/src/agents/tools/subagents-tool.test.ts @@ -37,7 +37,9 @@ describe("subagents tool", () => { it("advertises the unified task ledger", () => { const tool = createSubagentsTool(); - expect(tool.description).toBe("Background work: subagents, media gen, cron runs. list/cancel."); + expect(tool.description).toBe( + "Background work: subagents, media gen, automation runs. list/cancel.", + ); }); it("lists cross-runtime tasks in the caller session tree", async () => { diff --git a/src/agents/tools/subagents-tool.ts b/src/agents/tools/subagents-tool.ts index b04d185493ae..438d964ca4c6 100644 --- a/src/agents/tools/subagents-tool.ts +++ b/src/agents/tools/subagents-tool.ts @@ -89,7 +89,7 @@ export function createSubagentsTool(opts: SubagentsToolOptions = {}): AnyAgentTo return { label: "Subagents", name: "subagents", - description: "Background work: subagents, media gen, cron runs. list/cancel.", + description: "Background work: subagents, media gen, automation runs. list/cancel.", parameters: SubagentsToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; diff --git a/src/auto-reply/heartbeat.ts b/src/auto-reply/heartbeat.ts index a43b031db5af..b4863abdc170 100644 --- a/src/auto-reply/heartbeat.ts +++ b/src/auto-reply/heartbeat.ts @@ -6,7 +6,7 @@ import { HEARTBEAT_TOKEN } from "./tokens.js"; // Default heartbeat prompt (used when config.agents.defaults.heartbeat.prompt is unset). // Keep it tight and avoid encouraging the model to invent/rehash "open loops" from prior chat context. export const HEARTBEAT_CRON_TASK_GUIDANCE = - "Recurring tasks are cron jobs; create or change their schedules with cron tools or the openclaw cron CLI, not heartbeat scratch."; + "Recurring tasks are automations; create or change their schedules with the automations tool, not heartbeat scratch."; const HEARTBEAT_CONTEXT_PROMPT = `Follow the heartbeat monitor scratch context when provided. ${HEARTBEAT_CRON_TASK_GUIDANCE} Do not infer or repeat old tasks from prior chats.`; /** Default prompt for heartbeat turns when config does not override it. */ export const HEARTBEAT_PROMPT = `${HEARTBEAT_CONTEXT_PROMPT} If nothing needs attention, reply HEARTBEAT_OK.`; diff --git a/src/auto-reply/reply/commands-loop.test.ts b/src/auto-reply/reply/commands-loop.test.ts index bbd30cf43a53..4f240498bc95 100644 --- a/src/auto-reply/reply/commands-loop.test.ts +++ b/src/auto-reply/reply/commands-loop.test.ts @@ -74,6 +74,10 @@ describe("loop command", () => { expect(rewrittenBody(params)).toContain('sessionTarget:"current"'); expect(rewrittenBody(params)).toContain(`"${LOOP_PREFIX} check ci"`); expect(rewrittenBody(params)).toContain("do not use the message tool"); + // Loop work orders are model-facing prompts: they must teach the canonical + // automations tool, never the legacy cron alias (RFC 0026). + expect(rewrittenBody(params)).toContain("with the automations tool"); + expect(rewrittenBody(params)).not.toMatch(/\bcron tool\b/); }); it("preserves parseDurationMs unitless millisecond intervals", async () => { @@ -98,6 +102,8 @@ describe("loop command", () => { expect(await handleLoopCommand(params, true)).toEqual({ shouldContinue: true }); expect(rewrittenBody(params)).toContain('action:"list", includeDisabled:true'); expect(rewrittenBody(params)).toContain(`name starts with "${LOOP_PREFIX}"`); + expect(rewrittenBody(params)).toContain("Use the automations tool"); + expect(rewrittenBody(params)).not.toMatch(/\bcron\b/i); }); it("rewrites /loop stop as a scoped cron removal work order", async () => { diff --git a/src/auto-reply/reply/commands-loop.ts b/src/auto-reply/reply/commands-loop.ts index 58acf1360b36..728f0647ddf5 100644 --- a/src/auto-reply/reply/commands-loop.ts +++ b/src/auto-reply/reply/commands-loop.ts @@ -1,5 +1,6 @@ // Handles /loop by rewriting chat sugar into a cron-tool work order. import { createHash } from "node:crypto"; +import { AUTOMATIONS_TOOL_NAME } from "../../agents/tools/automations-tool-name.js"; import { parseDurationMs } from "../../cli/parse-duration.js"; import { truncateUtf16Safe } from "../../utils.js"; import { applyCommandTextToParams } from "./command-context-rewrite.js"; @@ -49,7 +50,7 @@ function buildLoopPayloadMessage(params: { ]; if (params.selfPaced) { lines.push( - 'Before replying, ALWAYS call the cron tool action:"next_check" with in:"" — pick the next check interval from how active the task is; back off toward 1h when quiet.', + `Before replying, ALWAYS call the ${AUTOMATIONS_TOOL_NAME} tool action:"next_check" with in:"" — pick the next check interval from how active the task is; back off toward 1h when quiet.`, ); } return lines.join("\n"); @@ -59,19 +60,19 @@ function buildFixedLoopWorkOrder(prompt: string, everyMs: number, sessionKey: st const shortName = loopShortName(prompt); const jobName = `${loopNamePrefix(sessionKey)} ${shortName}`; const message = buildLoopPayloadMessage({ prompt, shortName, selfPaced: false }); - return `Create a recurring loop with the cron tool, then confirm in one short line (name + cadence + '/loop stop' hint). ${LOOP_FINAL_REPLY_ONLY} action:"add", job:{name:${JSON.stringify(jobName)},schedule:{kind:"every",everyMs:${everyMs}},sessionTarget:"current",payload:{kind:"agentTurn",message:${JSON.stringify(message)}}}.`; + return `Create a recurring loop with the ${AUTOMATIONS_TOOL_NAME} tool, then confirm in one short line (name + cadence + '/loop stop' hint). ${LOOP_FINAL_REPLY_ONLY} action:"add", job:{name:${JSON.stringify(jobName)},schedule:{kind:"every",everyMs:${everyMs}},sessionTarget:"current",payload:{kind:"agentTurn",message:${JSON.stringify(message)}}}.`; } function buildSelfPacedLoopWorkOrder(prompt: string, sessionKey: string): string { const shortName = loopShortName(prompt); const jobName = `${loopNamePrefix(sessionKey)} ${shortName}`; const message = buildLoopPayloadMessage({ prompt, shortName, selfPaced: true }); - return `Create a recurring loop with the cron tool, then confirm in one short line (name + cadence + '/loop stop' hint). ${LOOP_FINAL_REPLY_ONLY} action:"add", job:{name:${JSON.stringify(jobName)},schedule:{kind:"every",everyMs:${LOOP_DEFAULT_INTERVAL_MS}},pacing:{min:"1m",max:"1h"},sessionTarget:"current",payload:{kind:"agentTurn",message:${JSON.stringify(message)}}}.`; + return `Create a recurring loop with the ${AUTOMATIONS_TOOL_NAME} tool, then confirm in one short line (name + cadence + '/loop stop' hint). ${LOOP_FINAL_REPLY_ONLY} action:"add", job:{name:${JSON.stringify(jobName)},schedule:{kind:"every",everyMs:${LOOP_DEFAULT_INTERVAL_MS}},pacing:{min:"1m",max:"1h"},sessionTarget:"current",payload:{kind:"agentTurn",message:${JSON.stringify(message)}}}.`; } function buildLoopStatusWorkOrder(sessionKey: string): string { const prefix = loopNamePrefix(sessionKey); - return `Use the cron tool (action:"list", includeDisabled:true) and report this conversation's loop jobs — exactly those whose name starts with ${JSON.stringify(prefix)}: name, schedule/pacing, enabled, last run, next run. If none, say so. ${LOOP_FINAL_REPLY_ONLY}`; + return `Use the ${AUTOMATIONS_TOOL_NAME} tool (action:"list", includeDisabled:true) and report this conversation's loop jobs — exactly those whose name starts with ${JSON.stringify(prefix)}: name, schedule/pacing, enabled, last run, next run. If none, say so. ${LOOP_FINAL_REPLY_ONLY}`; } function buildLoopStopWorkOrder(name: string, sessionKey: string): string { @@ -79,7 +80,7 @@ function buildLoopStopWorkOrder(name: string, sessionKey: string): string { const matchInstruction = name ? ` Among those, match ${JSON.stringify(name)} against the job name.` : ""; - return `List cron jobs (action:"list", includeDisabled:true) and find this conversation's loops — exactly those whose name starts with ${JSON.stringify(prefix)}.${matchInstruction} Remove the matching jobs with action:"remove" and confirm the removed names. If none matched, say so and list this conversation's active loop names. Never remove a job whose name does not start with ${JSON.stringify(prefix)}. ${LOOP_FINAL_REPLY_ONLY}`; + return `List automations (action:"list", includeDisabled:true) and find this conversation's loops — exactly those whose name starts with ${JSON.stringify(prefix)}.${matchInstruction} Remove the matching jobs with action:"remove" and confirm the removed names. If none matched, say so and list this conversation's active loop names. Never remove a job whose name does not start with ${JSON.stringify(prefix)}. ${LOOP_FINAL_REPLY_ONLY}`; } /** Command handler for conversation-bound recurring loops. */ diff --git a/src/claws/cron.ts b/src/claws/cron.ts index c81860451698..35e549af2778 100644 --- a/src/claws/cron.ts +++ b/src/claws/cron.ts @@ -293,7 +293,7 @@ export async function installClawCronJobs( if (!options.gateway) { throw new ClawCronInstallError( "cron_gateway_required", - "Claw cron jobs require the gateway-owned cron.add API.", + "Claw automations require the gateway-owned cron.add API.", [], ); } diff --git a/src/claws/doctor.ts b/src/claws/doctor.ts index d75a8b5f0846..70819f12557c 100644 --- a/src/claws/doctor.ts +++ b/src/claws/doctor.ts @@ -225,7 +225,7 @@ function collectInstallFindings( target: cron.schedulerJobId, requirement: "Claw cron health requires live Gateway corroboration by scheduler job id", fixHint: - "Recreate or reconcile the Gateway cron job before treating this Claw as healthy.", + "Recreate or reconcile the Gateway automation before treating this Claw as healthy.", }), ); continue; diff --git a/src/commands/doctor-heartbeat-cadence-migration.ts b/src/commands/doctor-heartbeat-cadence-migration.ts index 5ea1de71cc89..7e2d0e6805e0 100644 --- a/src/commands/doctor-heartbeat-cadence-migration.ts +++ b/src/commands/doctor-heartbeat-cadence-migration.ts @@ -48,7 +48,7 @@ function createDoctorCronService(storePath: string, cfg: OpenClawConfig): CronSe requestHeartbeat: noop, runIsolatedAgentJob: async () => ({ status: "skipped", - error: "doctor does not execute cron jobs", + error: "doctor does not execute automations", }), }); } diff --git a/src/commands/doctor-heartbeat-task-migration.ts b/src/commands/doctor-heartbeat-task-migration.ts index 7ba518d5b506..591ae25b68b6 100644 --- a/src/commands/doctor-heartbeat-task-migration.ts +++ b/src/commands/doctor-heartbeat-task-migration.ts @@ -82,7 +82,7 @@ function migrationFinding(params: { path: params.storePath, target: params.agentId, requirement: params.requirement, - fixHint: `Run ${formatCliCommand("openclaw doctor --fix")} to convert heartbeat tasks into cron jobs.`, + fixHint: `Run ${formatCliCommand("openclaw doctor --fix")} to convert heartbeat tasks into automations.`, }; } diff --git a/src/commands/doctor-sandbox.ts b/src/commands/doctor-sandbox.ts index 078ff52e2a4c..77b715eaf0e4 100644 --- a/src/commands/doctor-sandbox.ts +++ b/src/commands/doctor-sandbox.ts @@ -311,7 +311,7 @@ export async function maybeRepairSandboxImages( const lines = [ `Sandbox mode is enabled (mode: "${mode}") but Docker is not available.`, "Docker is required for sandbox mode to function.", - "Isolated sessions (cron jobs, sub-agents) will fail without Docker.", + "Isolated sessions (automations, sub-agents) will fail without Docker.", "", "Options:", "- Install Docker and restart the gateway", diff --git a/src/commands/doctor/cron/legacy-notify.ts b/src/commands/doctor/cron/legacy-notify.ts index 17bdabe53305..30048f77c463 100644 --- a/src/commands/doctor/cron/legacy-notify.ts +++ b/src/commands/doctor/cron/legacy-notify.ts @@ -72,7 +72,7 @@ export function migrateLegacyNotifyFallback(params: { if (configuredLegacyWebhook && !legacyWebhook) { // Keep the marker so doctor can retry after the operator fixes the target. warnings.push( - `Cron job "${jobName}" still uses legacy notify fallback, but cron.webhook is not a valid HTTP(S) URL so doctor cannot migrate it automatically.`, + `Automation "${jobName}" still uses legacy notify fallback, but cron.webhook is not a valid HTTP(S) URL so doctor cannot migrate it automatically.`, ); continue; } diff --git a/src/cron/isolated-agent.model-formatting.test.ts b/src/cron/isolated-agent.model-formatting.test.ts index 101ba9610544..088aa3c05a03 100644 --- a/src/cron/isolated-agent.model-formatting.test.ts +++ b/src/cron/isolated-agent.model-formatting.test.ts @@ -238,7 +238,7 @@ describe("cron model formatting and precedence edge cases", () => { }), ).resolves.toEqual({ ok: false, - error: "cron payload.model 'openai/' rejected: invalid model", + error: "automation model override 'openai/' rejected: invalid model", }); }); @@ -249,7 +249,7 @@ describe("cron model formatting and precedence edge cases", () => { }), ).resolves.toEqual({ ok: false, - error: "cron payload.model '/gpt-4.1-mini' rejected: invalid model", + error: "automation model override '/gpt-4.1-mini' rejected: invalid model", }); }); @@ -269,7 +269,7 @@ describe("cron model formatting and precedence edge cases", () => { ).resolves.toEqual({ ok: false, error: - "cron payload.model 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.modelPolicy.allow: anthropic/claude-sonnet-4-6 is not in [(none configured)]", + "automation model override 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.modelPolicy.allow: anthropic/claude-sonnet-4-6 is not in [(none configured)]", }); }); @@ -291,7 +291,7 @@ describe("cron model formatting and precedence edge cases", () => { ).resolves.toEqual({ ok: false, error: - "cron payload.model 'openai/gpt-5.5' rejected by agents.entries.*.modelPolicy.allow: openai/gpt-5.5 is not in [anthropic/*]", + "automation model override 'openai/gpt-5.5' rejected by agents.entries.*.modelPolicy.allow: openai/gpt-5.5 is not in [anthropic/*]", }); }); diff --git a/src/cron/isolated-agent/delivery-dispatch-awareness.ts b/src/cron/isolated-agent/delivery-dispatch-awareness.ts index dbde603aeb9a..9af7057c097b 100644 --- a/src/cron/isolated-agent/delivery-dispatch-awareness.ts +++ b/src/cron/isolated-agent/delivery-dispatch-awareness.ts @@ -154,7 +154,7 @@ export function resolveDirectCronFallbackSourceIndex( } function formatTargetCronDeliveryAwarenessText(text: string): string { - return `A scheduled cron job delivered this message to this channel:\n${text}`; + return `A scheduled automation delivered this message to this channel:\n${text}`; } export function formatTargetCronDeliveryFailureAwarenessText(params: { @@ -170,7 +170,7 @@ export function formatTargetCronDeliveryFailureAwarenessText(params: { targetParts.push(`thread ${params.threadId}`); } return [ - "A scheduled cron job attempted to deliver to this channel, but delivery failed.", + "A scheduled automation attempted to deliver to this channel, but delivery failed.", `Job: ${params.job.name || params.job.id}`, `Target: ${targetParts.join(" ")}`, `Delivery error: ${formatErrorMessage(params.error)}`, diff --git a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts index 5d8e5b56fee4..37b1b3320fcc 100644 --- a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts +++ b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts @@ -754,7 +754,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }), ); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( - "A scheduled cron job delivered this message to this channel:\nThreaded cron update.", + "A scheduled automation delivered this message to this channel:\nThreaded cron update.", { sessionKey: "agent:main:telegram:direct:123456:thread:42", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:42", @@ -788,7 +788,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( - "A scheduled cron job delivered this message to this channel:\nMain-scoped cron update.", + "A scheduled automation delivered this message to this channel:\nMain-scoped cron update.", { sessionKey: "agent:main", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", @@ -849,14 +849,14 @@ describe("dispatchCronDelivery — double-announce guard", () => { expect(enqueueSystemEvent).toHaveBeenCalledTimes(2); expect(enqueueSystemEvent).toHaveBeenCalledWith( - "A scheduled cron job delivered this message to this channel:\nShared cron update.", + "A scheduled automation delivered this message to this channel:\nShared cron update.", { sessionKey: "agent:main:telegram:direct:123456", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", }, ); expect(enqueueSystemEvent).toHaveBeenCalledWith( - "A scheduled cron job delivered this message to this channel:\nShared cron update.", + "A scheduled automation delivered this message to this channel:\nShared cron update.", { sessionKey: "agent:main:openclaw-weixin:direct:123456", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:openclaw-weixin::123456:", @@ -902,7 +902,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }), ); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( - "A scheduled cron job delivered this message to this channel:\nSession-targeted off-plan update.", + "A scheduled automation delivered this message to this channel:\nSession-targeted off-plan update.", { sessionKey: "agent:main:telegram:direct:123456", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", @@ -936,7 +936,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( - "A scheduled cron job delivered this message to this channel:\nweather-map.png", + "A scheduled automation delivered this message to this channel:\nweather-map.png", { sessionKey: "agent:main:telegram:direct:123456", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", @@ -987,7 +987,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }), ); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( - "A scheduled cron job delivered this message to this channel:\n386502", + "A scheduled automation delivered this message to this channel:\n386502", { sessionKey: "agent:main:openclaw-weixin:direct:user-123", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:openclaw-weixin::user-123:", @@ -1265,7 +1265,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", }); expect(enqueueSystemEvent).toHaveBeenCalledWith( - "A scheduled cron job delivered this message to this channel:\nRedacted cron update.", + "A scheduled automation delivered this message to this channel:\nRedacted cron update.", { sessionKey: "agent:main:telegram:direct:123456", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", @@ -1307,7 +1307,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", }); expect(enqueueSystemEvent).toHaveBeenCalledWith( - "A scheduled cron job delivered this message to this channel:\nMorning briefing complete.", + "A scheduled automation delivered this message to this channel:\nMorning briefing complete.", { sessionKey: "agent:main", contextKey: "cron-direct-delivery:v1:cron:test-job:1000:telegram::123456:", @@ -1580,7 +1580,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { expect(state.deliveryAttempted).toBe(true); expect(deliverOutboundPayloads).toHaveBeenCalledTimes(1); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( - "A scheduled cron job delivered this message to this channel:\nSession-bound cron update.", + "A scheduled automation delivered this message to this channel:\nSession-bound cron update.", { sessionKey: "agent:main", contextKey: expect.stringMatching( @@ -2145,7 +2145,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( [ - "A scheduled cron job attempted to deliver to this channel, but delivery failed.", + "A scheduled automation attempted to deliver to this channel, but delivery failed.", "Job: Test Job", "Target: telegram:123456", "Delivery error: second payload stopped before final dispatch | connect ECONNREFUSED | ECONNREFUSED", @@ -2520,7 +2520,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( [ - "A scheduled cron job attempted to deliver to this channel, but delivery failed.", + "A scheduled automation attempted to deliver to this channel, but delivery failed.", "Job: Test Job", "Target: telegram:123456 thread 42", "Delivery error: Call to 'sendMessage' failed! (400: Bad Request: message thread not found)", @@ -2567,7 +2567,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { }); expect(enqueueSystemEvent).toHaveBeenCalledExactlyOnceWith( [ - "A scheduled cron job attempted to deliver to this channel, but delivery failed.", + "A scheduled automation attempted to deliver to this channel, but delivery failed.", "Job: Test Job", "Target: telegram:123456", "Delivery error: second payload failed", diff --git a/src/cron/isolated-agent/model-preflight.runtime.ts b/src/cron/isolated-agent/model-preflight.runtime.ts index 8bc715c615b4..d58097db087b 100644 --- a/src/cron/isolated-agent/model-preflight.runtime.ts +++ b/src/cron/isolated-agent/model-preflight.runtime.ts @@ -171,8 +171,8 @@ function formatUnavailableReason(params: { error: unknown; }): string { return [ - `Agent cron job uses ${params.provider}/${params.model} but the local provider preflight failed at ${params.baseUrl}.`, - `The candidate is unavailable for this cron run; OpenClaw will retry its provider preflight on a later scheduled run.`, + `This automation uses ${params.provider}/${params.model} but the local provider preflight failed at ${params.baseUrl}.`, + `The candidate is unavailable for this run; OpenClaw will retry its provider preflight on a later scheduled run.`, `Last error: ${formatPreflightError(params.error)}`, ].join(" "); } diff --git a/src/cron/isolated-agent/model-selection.ts b/src/cron/isolated-agent/model-selection.ts index 101719efa14e..054b06549deb 100644 --- a/src/cron/isolated-agent/model-selection.ts +++ b/src/cron/isolated-agent/model-selection.ts @@ -72,9 +72,9 @@ function formatCronPayloadModelRejection(params: { const modelRef = error.slice("model not allowed:".length).trim(); const policy = resolveConfiguredModelPolicyAllow(params); const policyPath = policy.configPath ?? "agents.defaults.modelPolicy.allow"; - return `cron payload.model '${modelOverride}' rejected by ${policyPath}: ${modelRef} is not in [${formatAllowedModelRefs(params)}]`; + return `automation model override '${modelOverride}' rejected by ${policyPath}: ${modelRef} is not in [${formatAllowedModelRefs(params)}]`; } - return `cron payload.model '${modelOverride}' rejected: ${error}`; + return `automation model override '${modelOverride}' rejected: ${error}`; } export async function resolveCronModelSelectionOwner(params: { diff --git a/src/cron/isolated-agent/run-prepare-runtime.ts b/src/cron/isolated-agent/run-prepare-runtime.ts index e944c81ed3f7..8591bfbc49d1 100644 --- a/src/cron/isolated-agent/run-prepare-runtime.ts +++ b/src/cron/isolated-agent/run-prepare-runtime.ts @@ -112,6 +112,6 @@ export function appendCronUnattendedRunPreamble( ) { const core = `This is an unattended scheduled run. Nobody is present to clarify or approve, so complete the task with what you have. Your final reply is the deliverable — not a plan, an acknowledgement, or a request for input. If nothing needs doing, reply exactly ${HEARTBEAT_TOKEN}. If something failed, state plainly what failed and what you tried — the scheduler owns retries and failure alerts.`; const trustedExtra = - " Where the job's own instructions conflict with this preamble, the job's instructions win (a question or plan the job explicitly requests is a valid deliverable). If this job is no longer needed, you may remove it with the cron tool."; + " Where the job's own instructions conflict with this preamble, the job's instructions win (a question or plan the job explicitly requests is a valid deliverable). If this job is no longer needed, you may remove it with the automations tool."; return `${commandBody}\n\n${core}${opts.externalHook ? "" : trustedExtra}`; } diff --git a/src/cron/isolated-agent/run-prepare.ts b/src/cron/isolated-agent/run-prepare.ts index 363792a0f87e..fa0717375387 100644 --- a/src/cron/isolated-agent/run-prepare.ts +++ b/src/cron/isolated-agent/run-prepare.ts @@ -340,7 +340,7 @@ export async function prepareCronRunContext(params: { typeof input.job.name === "string" && input.job.name.trim() ? input.job.name.trim() : input.job.id; - cronSession.sessionEntry.label = `Cron: ${labelSuffix}`; + cronSession.sessionEntry.label = `Automation: ${labelSuffix}`; } const resolvedModelSelection = await resolveCronModelSelection({ diff --git a/src/cron/isolated-agent/run.cron-model-override.test.ts b/src/cron/isolated-agent/run.cron-model-override.test.ts index dea773b0f02b..c30779b81fa1 100644 --- a/src/cron/isolated-agent/run.cron-model-override.test.ts +++ b/src/cron/isolated-agent/run.cron-model-override.test.ts @@ -201,7 +201,7 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => { expect(result.status).toBe("error"); expect(result.error).toContain("Model not allowed"); expect(result.diagnostics?.summary).toBe( - "cron payload.model 'anthropic/claude-sonnet-4-6' rejected: Model not allowed: anthropic/claude-sonnet-4-6", + "automation model override 'anthropic/claude-sonnet-4-6' rejected: Model not allowed: anthropic/claude-sonnet-4-6", ); expect(result.diagnostics?.entries).toHaveLength(1); expect(result.diagnostics?.entries[0]?.ts).toBeTypeOf("number"); @@ -210,7 +210,7 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => { source: "cron-preflight", severity: "error", message: - "cron payload.model 'anthropic/claude-sonnet-4-6' rejected: Model not allowed: anthropic/claude-sonnet-4-6", + "automation model override 'anthropic/claude-sonnet-4-6' rejected: Model not allowed: anthropic/claude-sonnet-4-6", }); // Model should remain undefined — the early return happens before the // pre-run persist block, so neither the session entry nor the store diff --git a/src/cron/isolated-agent/run.message-tool-policy.test.ts b/src/cron/isolated-agent/run.message-tool-policy.test.ts index 3dd80cdb64b8..7856f12e5991 100644 --- a/src/cron/isolated-agent/run.message-tool-policy.test.ts +++ b/src/cron/isolated-agent/run.message-tool-policy.test.ts @@ -1592,7 +1592,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const prompt = expectEmbeddedRunPrompt(); const unattendedPreamble = - "This is an unattended scheduled run. Nobody is present to clarify or approve, so complete the task with what you have. Your final reply is the deliverable — not a plan, an acknowledgement, or a request for input. If nothing needs doing, reply exactly HEARTBEAT_OK. If something failed, state plainly what failed and what you tried — the scheduler owns retries and failure alerts. Where the job's own instructions conflict with this preamble, the job's instructions win (a question or plan the job explicitly requests is a valid deliverable). If this job is no longer needed, you may remove it with the cron tool."; + "This is an unattended scheduled run. Nobody is present to clarify or approve, so complete the task with what you have. Your final reply is the deliverable — not a plan, an acknowledgement, or a request for input. If nothing needs doing, reply exactly HEARTBEAT_OK. If something failed, state plainly what failed and what you tried — the scheduler owns retries and failure alerts. Where the job's own instructions conflict with this preamble, the job's instructions win (a question or plan the job explicitly requests is a valid deliverable). If this job is no longer needed, you may remove it with the automations tool."; expect(prompt).toContain(unattendedPreamble); expect(prompt).toContain("Use the message tool"); expect(prompt.indexOf(unattendedPreamble)).toBeLessThan(prompt.indexOf("Use the message tool")); @@ -1628,7 +1628,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { expect(prompt.indexOf("")).toBeLessThan( prompt.indexOf("This is an unattended scheduled run"), ); - expect(prompt).not.toContain("you may remove it with the cron tool"); + expect(prompt).not.toContain("you may remove it with the automations tool"); expect(prompt).not.toContain("the job's instructions win"); expect(buildSafeExternalPromptMock).toHaveBeenCalledWith( expect.objectContaining({ content: "send a message", jobName: "Message Tool Policy" }), diff --git a/src/cron/isolated-agent/run.skill-filter.test.ts b/src/cron/isolated-agent/run.skill-filter.test.ts index 350cb0e55f32..d24f4b46f00c 100644 --- a/src/cron/isolated-agent/run.skill-filter.test.ts +++ b/src/cron/isolated-agent/run.skill-filter.test.ts @@ -271,7 +271,7 @@ describe("runCronIsolatedAgentTurn — skill filter", () => { expect(result.status).toBe("error"); expect(result.error).toBe( - "cron payload.model 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.modelPolicy.allow: anthropic/claude-sonnet-4-6 is not in [openai/gpt-5.4]", + "automation model override 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.modelPolicy.allow: anthropic/claude-sonnet-4-6 is not in [openai/gpt-5.4]", ); expect(logWarnMock).not.toHaveBeenCalled(); expect(runWithModelFallbackMock).not.toHaveBeenCalled(); @@ -291,7 +291,9 @@ describe("runCronIsolatedAgentTurn — skill filter", () => { ); expect(result.status).toBe("error"); - expect(result.error).toBe("cron payload.model 'openai/' rejected: invalid model: openai/"); + expect(result.error).toBe( + "automation model override 'openai/' rejected: invalid model: openai/", + ); expect(logWarnMock).not.toHaveBeenCalled(); expect(runWithModelFallbackMock).not.toHaveBeenCalled(); }); diff --git a/src/cron/service.failure-alert.test.ts b/src/cron/service.failure-alert.test.ts index c23701746c5a..efc04502814b 100644 --- a/src/cron/service.failure-alert.test.ts +++ b/src/cron/service.failure-alert.test.ts @@ -134,7 +134,7 @@ describe("CronService failure alerts", () => { to: "19098680", }); expect((firstAlert.job as { id?: string } | undefined)?.id).toBe(job.id); - expectAlertTextContaining(sendCronFailureAlert, 'Cron job "daily report" failed 2 times'); + expectAlertTextContaining(sendCronFailureAlert, 'Automation "daily report" failed 2 times'); await cron.run(job.id, "force"); expect(sendCronFailureAlert).toHaveBeenCalledTimes(1); @@ -142,7 +142,7 @@ describe("CronService failure alerts", () => { vi.advanceTimersByTime(60_000); await cron.run(job.id, "force"); expect(sendCronFailureAlert).toHaveBeenCalledTimes(2); - expectAlertTextContaining(sendCronFailureAlert, 'Cron job "daily report" failed 4 times'); + expectAlertTextContaining(sendCronFailureAlert, 'Automation "daily report" failed 4 times'); cron.stop(); await store.cleanup(); @@ -289,7 +289,7 @@ describe("CronService failure alerts", () => { }); expectAlertTextContaining( sendCronFailureAlert, - 'Cron job "updated skipped alert job" skipped 1 times', + 'Automation "updated skipped alert job" skipped 1 times', ); cron.stop(); @@ -571,7 +571,7 @@ describe("CronService failure alerts", () => { expectAlertFields(sendCronFailureAlert, expected); expectAlertTextContaining( sendCronFailureAlert, - 'Cron job "globally routed failure alert" failed 1 times', + 'Automation "globally routed failure alert" failed 1 times', ); cron.stop(); @@ -689,7 +689,7 @@ describe("CronService failure alerts", () => { }); expectAlertTextContaining( sendCronFailureAlert, - 'Cron job "explicit job alert with a failure destination" failed 1 times', + 'Automation "explicit job alert with a failure destination" failed 1 times', ); cron.stop(); @@ -742,7 +742,7 @@ describe("CronService failure alerts", () => { }); expectAlertTextContaining( sendCronFailureAlert, - 'Cron job "skipped job with a failure destination" skipped 1 times', + 'Automation "skipped job with a failure destination" skipped 1 times', ); cron.stop(); @@ -796,7 +796,9 @@ describe("CronService failure alerts", () => { if (typeof alertText !== "string") { throw new Error("expected failure alert text"); } - expect(alertText).toMatch(/Cron job "gateway restart" skipped 2 times\nSkip reason: disabled/); + expect(alertText).toMatch( + /Automation "gateway restart" skipped 2 times\nSkip reason: disabled/, + ); const skippedJob = cron.getJob(job.id); expect(skippedJob?.state.consecutiveSkipped).toBe(2); @@ -841,7 +843,7 @@ describe("CronService failure alerts", () => { expect(sendCronFailureAlert).toHaveBeenCalledTimes(1); const alertText = alertCallArg(sendCronFailureAlert).text; expect(alertText).toBe( - 'Cron job "timeout cause alert" failed 1 times\n' + + 'Automation "timeout cause alert" failed 1 times\n' + "Cause: timeout\n" + "Last error: cron: job execution timed out", ); @@ -886,7 +888,7 @@ describe("CronService failure alerts", () => { expect(sendCronFailureAlert).toHaveBeenCalledTimes(1); const alertText = alertCallArg(sendCronFailureAlert).text; expect(alertText).toBe( - 'Cron job "provider limit alert" failed 1 times\n' + + 'Automation "provider limit alert" failed 1 times\n' + "Cause: billing\n" + "Last error: 403 Key limit exceeded (monthly limit)", ); @@ -930,7 +932,7 @@ describe("CronService failure alerts", () => { await cron.run(job.id, "force"); expect(sendCronFailureAlert).toHaveBeenCalledTimes(1); expect(alertCallArg(sendCronFailureAlert).text).toBe( - 'Cron job "permanent script alert" failed 1 times\n' + + 'Automation "permanent script alert" failed 1 times\n' + "Last error: cron script failed after a tool side effect: request timed out", ); @@ -974,7 +976,7 @@ describe("CronService failure alerts", () => { expect(sendCronFailureAlert).toHaveBeenCalledTimes(1); const alertText = alertCallArg(sendCronFailureAlert).text; expect(alertText).toBe( - 'Cron job "skipped timeout" skipped 1 times\nSkip reason: cron: job execution timed out', + 'Automation "skipped timeout" skipped 1 times\nSkip reason: cron: job execution timed out', ); cron.stop(); diff --git a/src/cron/service/failure-alerts.ts b/src/cron/service/failure-alerts.ts index e545c5c35766..2a9fd6298ab0 100644 --- a/src/cron/service/failure-alerts.ts +++ b/src/cron/service/failure-alerts.ts @@ -154,7 +154,7 @@ function emitFailureAlert( const statusVerb = params.status === "skipped" ? "skipped" : "failed"; const detailLabel = params.status === "skipped" ? "Skip reason" : "Last error"; const text = [ - `Cron job "${safeJobName}" ${statusVerb} ${params.consecutiveErrors} times`, + `Automation "${safeJobName}" ${statusVerb} ${params.consecutiveErrors} times`, ...(errorReason ? [`Cause: ${errorReason}`] : []), `${detailLabel}: ${truncatedError}`, ].join("\n"); diff --git a/src/cron/service/jobs-scheduling.ts b/src/cron/service/jobs-scheduling.ts index cdfabb179b2a..a371c3fab96f 100644 --- a/src/cron/service/jobs-scheduling.ts +++ b/src/cron/service/jobs-scheduling.ts @@ -384,7 +384,7 @@ export function recordScheduleComputeError(params: { "cron: auto-disabled job after repeated schedule errors", ); - const notifyText = `⚠️ Cron job "${job.name}" has been auto-disabled after ${errorCount} consecutive schedule errors. Last error: ${errText}`; + const notifyText = `⚠️ Automation "${job.name}" has been auto-disabled after ${errorCount} consecutive schedule errors. Last error: ${errText}`; const notify = () => { state.deps.enqueueSystemEvent(notifyText, { agentId: job.agentId, diff --git a/src/cron/service/normalize.ts b/src/cron/service/normalize.ts index 713f60e9dffd..c0121f56c1ca 100644 --- a/src/cron/service/normalize.ts +++ b/src/cron/service/normalize.ts @@ -6,11 +6,11 @@ import type { CronPayload } from "../types.js"; /** Normalizes a required cron job name and throws the public validation error when absent. */ export function normalizeRequiredName(raw: unknown) { if (typeof raw !== "string") { - throw new Error("cron job name is required"); + throw new Error("automation name is required"); } const name = raw.trim(); if (!name) { - throw new Error("cron job name is required"); + throw new Error("automation name is required"); } return name; } @@ -59,7 +59,7 @@ export function inferCronJobName(job: { if (kind === "at") { return "One-shot"; } - return "Cron job"; + return "Automation"; } /** Extracts the executable text from cron payload variants for main-session queueing. */ diff --git a/src/cron/service/ops.test.ts b/src/cron/service/ops.test.ts index dfd8c6db1946..658a0f7fa4a9 100644 --- a/src/cron/service/ops.test.ts +++ b/src/cron/service/ops.test.ts @@ -921,7 +921,7 @@ describe("cron service ops seam coverage", () => { runtime: "cron", status: "succeeded", sourceId: "isolated-timeout", - progressSummary: "Running cron job.", + progressSummary: "Running automation.", }); expect(findTaskByRunId(manualRunId)).toBeUndefined(); }); @@ -1207,7 +1207,7 @@ describe("cron service ops seam coverage", () => { runtime: "cron", status: "timed_out", sourceId: "startup-timeout", - progressSummary: "Running cron job.", + progressSummary: "Running automation.", }); }); }); @@ -1244,8 +1244,8 @@ describe("cron service ops seam coverage", () => { throw new Error("expected active manual cron task ledger record"); } expect(task.status).toBe("running"); - expect(task.progressSummary).toBe("Running cron job."); - expect(formatTaskStatusDetail(task)).toBe("Running cron job."); + expect(task.progressSummary).toBe("Running automation."); + expect(formatTaskStatusDetail(task)).toBe("Running automation."); resolveRun?.({ status: "ok", summary: "done" }); await manualRun; diff --git a/src/cron/service/task-ledger.ts b/src/cron/service/task-ledger.ts index 6397a9c3c7f6..248ad0a60fba 100644 --- a/src/cron/service/task-ledger.ts +++ b/src/cron/service/task-ledger.ts @@ -1,2 +1,2 @@ -/** Progress summary shown while a detached task ledger row represents an active cron run. */ -export const CRON_TASK_RUNNING_PROGRESS_SUMMARY = "Running cron job."; +/** Progress summary shown while a detached task ledger row represents an active automation run. */ +export const CRON_TASK_RUNNING_PROGRESS_SUMMARY = "Running automation."; diff --git a/src/cron/service/timer.test.ts b/src/cron/service/timer.test.ts index ee3e8127b44b..a43e30044849 100644 --- a/src/cron/service/timer.test.ts +++ b/src/cron/service/timer.test.ts @@ -780,8 +780,8 @@ describe("cron service timer seam coverage", () => { throw new Error("expected active cron task ledger record"); } expect(task.status).toBe("running"); - expect(task.progressSummary).toBe("Running cron job."); - expect(formatTaskStatusDetail(task)).toBe("Running cron job."); + expect(task.progressSummary).toBe("Running automation."); + expect(formatTaskStatusDetail(task)).toBe("Running automation."); resolveRun?.({ status: "ok", summary: "done" }); await timerRun; diff --git a/src/gateway/server-cron-notifications.test.ts b/src/gateway/server-cron-notifications.test.ts index 661109fdc874..0be5894d9f52 100644 --- a/src/gateway/server-cron-notifications.test.ts +++ b/src/gateway/server-cron-notifications.test.ts @@ -612,7 +612,7 @@ describe("dispatchGatewayCronFinishedNotifications", () => { "main", announceJob.id, expect.anything(), - '⚠️ Cron job "notification admission" failed: provider unavailable\nRun started: 2026-01-15 10:30 EST', + '⚠️ Automation "notification admission" failed: provider unavailable\nRun started: 2026-01-15 10:30 EST', ); vi.clearAllMocks(); @@ -642,7 +642,7 @@ describe("dispatchGatewayCronFinishedNotifications", () => { await waitForFast(() => expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledOnce()); expect(webhookRequestBody()).toMatchObject({ - message: 'Cron job "notification admission" failed: provider unavailable', + message: 'Automation "notification admission" failed: provider unavailable', runAtMs, }); }); @@ -813,7 +813,7 @@ describe("dispatchGatewayCronFinishedNotifications", () => { sessionKey: undefined, inheritSessionThread: false, }, - '⚠️ Cron job "channel fd no mode" failed: boom', + '⚠️ Automation "channel fd no mode" failed: boom', ); expect(logger.warn).not.toHaveBeenCalledWith( expect.objectContaining({ jobId: job.id }), diff --git a/src/gateway/server-cron-notifications.ts b/src/gateway/server-cron-notifications.ts index 00a803bf3867..2f467d630aec 100644 --- a/src/gateway/server-cron-notifications.ts +++ b/src/gateway/server-cron-notifications.ts @@ -166,7 +166,7 @@ function buildCronWebhookHeaders(webhookToken?: string): Record } function buildCronFailureWebhookPayload(params: { evt: CronEvent; job: CronJob }) { - const failureMessage = `Cron job "${params.job.name}" failed: ${params.evt.error ?? "unknown error"}`; + const failureMessage = `Automation "${params.job.name}" failed: ${params.evt.error ?? "unknown error"}`; return { jobId: params.job.id, jobName: params.job.name, diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index a0facfcbb169..530d01e40175 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -1811,7 +1811,7 @@ describe("gateway server cron", () => { expect(failureDestCall.url).toBe("https://example.invalid/failure-destination"); const failureDestBody = failureDestCall.body; expect(failureDestBody.message).toBe( - 'Cron job "failure destination webhook" failed: unknown error', + 'Automation "failure destination webhook" failed: unknown error', ); fetchWithSsrFGuardMock.mockClear(); @@ -2002,7 +2002,7 @@ describe("gateway server cron", () => { jobId, channel: "last", sessionKey: "agent:main:telegram:direct:123:thread:99", - message: '⚠️ Cron job "primary delivery fallback" failed: unknown error', + message: '⚠️ Automation "primary delivery fallback" failed: unknown error', includeRunStarted: true, }); } finally { @@ -2061,7 +2061,7 @@ describe("gateway server cron", () => { to: "#alerts", sessionKey: undefined, inheritSessionThread: false, - message: '⚠️ Cron job "channel fd no mode" failed: unknown error', + message: '⚠️ Automation "channel fd no mode" failed: unknown error', includeRunStarted: true, }); expect(fetchWithSsrFGuardMock).not.toHaveBeenCalled(); @@ -2114,7 +2114,7 @@ describe("gateway server cron", () => { jobId, channel: "last", sessionKey: "agent:avery:feishu:direct:ou_founder", - message: '⚠️ Cron job "session target failure fallback" failed: unknown error', + message: '⚠️ Automation "session target failure fallback" failed: unknown error', includeRunStarted: true, }); } finally { diff --git a/src/gateway/server/hooks.agent-trust.test.ts b/src/gateway/server/hooks.agent-trust.test.ts index 1b533a29b88f..45f364cea650 100644 --- a/src/gateway/server/hooks.agent-trust.test.ts +++ b/src/gateway/server/hooks.agent-trust.test.ts @@ -613,7 +613,7 @@ describe("dispatchAgentHook trust handling", () => { it("prefers cron diagnostics for returned hook errors", async () => { const diagnosticSummary = - "cron payload.model 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.modelPolicy.allow: anthropic/claude-sonnet-4-6"; + "automation model override 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.modelPolicy.allow: anthropic/claude-sonnet-4-6"; runCronIsolatedAgentTurnMock.mockResolvedValueOnce({ status: "error", summary: "generic failure", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json index 9dc8d5b3bc8c..bbd1da944c33 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json @@ -265,7 +265,7 @@ "tools": [ { "deferLoading": true, - "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted cron-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", + "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", "inputSchema": { "additionalProperties": true, "properties": { @@ -1461,7 +1461,7 @@ }, { "deferLoading": true, - "description": "Background work: subagents, media gen, cron runs. list/cancel.", + "description": "Background work: subagents, media gen, automation runs. list/cancel.", "inputSchema": { "properties": { "action": { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json index 9ab390452ba6..5661f7885102 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json @@ -261,7 +261,7 @@ "tools": [ { "deferLoading": true, - "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted cron-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", + "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", "inputSchema": { "additionalProperties": true, "properties": { @@ -1457,7 +1457,7 @@ }, { "deferLoading": true, - "description": "Background work: subagents, media gen, cron runs. list/cancel.", + "description": "Background work: subagents, media gen, automation runs. list/cancel.", "inputSchema": { "properties": { "action": { @@ -1631,7 +1631,7 @@ "type": "string" }, "scratch": { - "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in cron jobs, not scratch.", + "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", "type": "string" }, "summary": { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json index 1009dd7a792d..0bd56396efa6 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json @@ -261,7 +261,7 @@ "tools": [ { "deferLoading": true, - "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted cron-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", + "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId patch | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", "inputSchema": { "additionalProperties": true, "properties": { @@ -1457,7 +1457,7 @@ }, { "deferLoading": true, - "description": "Background work: subagents, media gen, cron runs. list/cancel.", + "description": "Background work: subagents, media gen, automation runs. list/cancel.", "inputSchema": { "properties": { "action": { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 04381ac62540..13b017c8a446 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 61444, - "roughTokens": 15361 + "chars": 61456, + "roughTokens": 15364 }, "openClawDeveloperInstructions": { "chars": 3811, @@ -233,8 +233,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 7049 }, "totalWithDynamicToolsJson": { - "chars": 89640, - "roughTokens": 22410 + "chars": 89652, + "roughTokens": 22413 }, "userInputText": { "chars": 1300, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index a92aa0f63441..7ab0c9a97798 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 61136, - "roughTokens": 15284 + "chars": 61148, + "roughTokens": 15287 }, "openClawDeveloperInstructions": { "chars": 2702, @@ -233,8 +233,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6679 }, "totalWithDynamicToolsJson": { - "chars": 87852, - "roughTokens": 21963 + "chars": 87864, + "roughTokens": 21966 }, "userInputText": { "chars": 929, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 05c13e6e77be..2630efd9c5db 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -222,24 +222,24 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 62668, - "roughTokens": 15667 + "chars": 62682, + "roughTokens": 15671 }, "openClawDeveloperInstructions": { "chars": 2702, "roughTokens": 676 }, "totalTextOnly": { - "chars": 27143, - "roughTokens": 6786 + "chars": 27130, + "roughTokens": 6783 }, "totalWithDynamicToolsJson": { - "chars": 89813, + "chars": 89814, "roughTokens": 22454 }, "userInputText": { - "chars": 1284, - "roughTokens": 321 + "chars": 1271, + "roughTokens": 318 } } ``` @@ -507,7 +507,7 @@ Conversation info: ⟦openclaw:ctx⟧ {"chat_id":"user:1000001","message_id":"heartbeat-0001","sender":{"id":"1000001","name":"Pash","username":"pash"}} ``` -Follow the heartbeat monitor scratch context when provided. Recurring tasks are cron jobs; create or change their schedules with cron tools or the openclaw cron CLI, not heartbeat scratch. Do not infer or repeat old tasks from prior chats. Use heartbeat_respond to report the wake outcome. Set notify=false when nothing needs the user's attention. Set notify=true with notificationText only when the user should be interrupted. +Follow the heartbeat monitor scratch context when provided. Recurring tasks are automations; create or change their schedules with the automations tool, not heartbeat scratch. Do not infer or repeat old tasks from prior chats. Use heartbeat_respond to report the wake outcome. Set notify=false when nothing needs the user's attention. Set notify=true with notificationText only when the user should be interrupted. ```` ### Tools: Dynamic Tool Catalog @@ -695,7 +695,7 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "type": "string" }, "scratch": { - "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in cron jobs, not scratch.", + "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", "type": "string" }, "summary": {