diff --git a/CHANGELOG.md b/CHANGELOG.md index 045c069d218..9b98966ed03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai - Node host/service auth env: include `OPENCLAW_GATEWAY_TOKEN` in `openclaw node install` service environments (with `CLAWDBOT_GATEWAY_TOKEN` compatibility fallback) so installed node services keep remote gateway token auth across restart/reboot. Fixes #31041. Thanks @OneStepAt4time for reporting, @byungsker, @liuxiaopai-ai, and @vincentkoc. - Security/Subagents sandbox inheritance: block sandboxed sessions from spawning cross-agent subagents that would run unsandboxed, preventing runtime sandbox downgrade via `sessions_spawn agentId`. Thanks @tdjackey for reporting. - Security/Workspace safe writes: harden `writeFileWithinRoot` against symlink-retarget TOCTOU races by opening existing files without truncation, creating missing files with exclusive create, deferring truncation until post-open identity+boundary validation, and removing out-of-root create artifacts on blocked races; added regression tests for truncate/create race paths. This ships in the next npm release (`2026.3.1`). Thanks @tdjackey for reporting. +- Control UI/Cron editor: include `{ mode: "none" }` in `cron.update` patches when editing an existing job and selecting “Result delivery = None (internal)”, so saved jobs no longer keep stale announce delivery mode. Fixes #31075. - Security/Node metadata policy: harden node platform classification against Unicode confusables and switch unknown platform defaults to a conservative allowlist that excludes `system.run`/`system.which` unless explicitly allowlisted, preventing metadata canonicalization drift from broadening node command permissions. Thanks @tdjackey for reporting. - Plugins/Discovery precedence: load bundled plugins before auto-discovered global extensions so bundled channel plugins win duplicate-ID resolution by default (explicit `plugins.load.paths` overrides remain highest precedence), with loader regression coverage. Landed from contributor PR #29710 by @Sid-Qin. Thanks @Sid-Qin. - Discord/Reconnect integrity: release Discord message listener lane immediately while preserving serialized handler execution, add HELLO-stall resume-first recovery with bounded fresh-identify fallback after repeated stalls, and extend lifecycle/listener regression coverage for forced reconnect scenarios. Landed from contributor PR #29508 by @cgdusek. Thanks @cgdusek. diff --git a/ui/src/ui/controllers/cron.test.ts b/ui/src/ui/controllers/cron.test.ts index 223eceb7c94..d8e55589360 100644 --- a/ui/src/ui/controllers/cron.test.ts +++ b/ui/src/ui/controllers/cron.test.ts @@ -210,6 +210,7 @@ describe("cron controller", () => { deleteAfterRun: false, schedule: { kind: "cron", expr: "0 8 * * *", staggerMs: 0 }, payload: { kind: "systemEvent", text: "updated" }, + delivery: { mode: "none" }, }, }); expect(state.cronEditingJobId).toBeNull(); diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index 72d9ba8a91a..16d84c1933f 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -614,17 +614,21 @@ export async function addCronJob(state: CronState) { const payload = buildCronPayload(form); const selectedDeliveryMode = form.deliveryMode; const delivery = - selectedDeliveryMode && selectedDeliveryMode !== "none" - ? { - mode: selectedDeliveryMode, - channel: - selectedDeliveryMode === "announce" - ? form.deliveryChannel.trim() || "last" - : undefined, - to: form.deliveryTo.trim() || undefined, - bestEffort: form.deliveryBestEffort, - } - : undefined; + selectedDeliveryMode === "none" + ? state.cronEditingJobId + ? { mode: "none" as const } + : undefined + : selectedDeliveryMode + ? { + mode: selectedDeliveryMode, + channel: + selectedDeliveryMode === "announce" + ? form.deliveryChannel.trim() || "last" + : undefined, + to: form.deliveryTo.trim() || undefined, + bestEffort: form.deliveryBestEffort, + } + : undefined; const failureAlert = buildFailureAlert(form); const agentId = form.clearAgent ? null : form.agentId.trim(); const job = {