diff --git a/CHANGELOG.md b/CHANGELOG.md index 23741207ef6..847bf215a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,7 @@ Docs: https://docs.openclaw.ai - Cron/Failure alerts: add configurable repeated-failure alerting with per-job overrides and Web UI cron editor support (`inherit|disabled|custom` with threshold/cooldown/channel/target fields). (#24789) Thanks @0xbrak. - Cron/Isolated model defaults: resolve isolated cron `subagents.model` (including object-form `primary`) through allowlist-aware model selection so isolated cron runs honor subagent model defaults unless explicitly overridden by job payload model. (#11474) Thanks @AnonO6. - Cron/Isolated sessions list: persist the intended pre-run model/provider on isolated cron session entries so `sessions_list` reflects payload/session model overrides even when runs fail before post-run telemetry persistence. (#21279) Thanks @altaywtf. +- Cron tool/update flat params: recover top-level update patch fields when models omit the `patch` wrapper, and allow flattened update keys through tool input schema validation so `cron.update` no longer fails with `patch required` for valid flat payloads. (#23221) - Agents/Message tool scoping: include other configured channels in scoped `message` tool action enum + description so isolated/cron runs can discover and invoke cross-channel actions without schema validation failures. Landed from contributor PR #20840 by @altaywtf. Thanks @altaywtf. - Web UI/Chat sessions: add a cron-session visibility toggle in the session selector, fix cron-key detection across `cron:*` and `agent:*:cron:*` formats, and localize the new control labels/tooltips. (#26976) Thanks @ianderrington. - Web UI/Cron jobs: add schedule-kind and last-run-status filters to the Jobs list, with reset control and client-side filtering over loaded results. (#9510) Thanks @guxu11. diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index d1a1bb429bc..6d615b47945 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -512,4 +512,50 @@ describe("cron tool", () => { ).rejects.toThrow('delivery.mode="webhook" requires delivery.to to be a valid http(s) URL'); expect(callGatewayMock).toHaveBeenCalledTimes(0); }); + + it("recovers flat patch params for update action", async () => { + callGatewayMock.mockResolvedValueOnce({ ok: true }); + + const tool = createCronTool(); + await tool.execute("call-update-flat", { + action: "update", + jobId: "job-1", + name: "new-name", + enabled: false, + }); + + expect(callGatewayMock).toHaveBeenCalledTimes(1); + const call = callGatewayMock.mock.calls[0]?.[0] as { + method?: string; + params?: { id?: string; patch?: { name?: string; enabled?: boolean } }; + }; + expect(call.method).toBe("cron.update"); + expect(call.params?.id).toBe("job-1"); + expect(call.params?.patch?.name).toBe("new-name"); + expect(call.params?.patch?.enabled).toBe(false); + }); + + it("recovers additional flat patch params for update action", async () => { + callGatewayMock.mockResolvedValueOnce({ ok: true }); + + const tool = createCronTool(); + await tool.execute("call-update-flat-extra", { + action: "update", + id: "job-2", + sessionTarget: "main", + failureAlert: { after: 3, cooldownMs: 60_000 }, + }); + + const call = callGatewayMock.mock.calls[0]?.[0] as { + method?: string; + params?: { + id?: string; + patch?: { sessionTarget?: string; failureAlert?: { after?: number; cooldownMs?: number } }; + }; + }; + expect(call.method).toBe("cron.update"); + expect(call.params?.id).toBe("job-2"); + expect(call.params?.patch?.sessionTarget).toBe("main"); + expect(call.params?.patch?.failureAlert).toEqual({ after: 3, cooldownMs: 60_000 }); + }); }); diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index d2a019c21e6..14df6901024 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -28,23 +28,26 @@ const REMINDER_CONTEXT_TOTAL_MAX = 700; const REMINDER_CONTEXT_MARKER = "\n\nRecent context:\n"; // Flattened schema: runtime validates per-action requirements. -const CronToolSchema = Type.Object({ - action: stringEnum(CRON_ACTIONS), - gatewayUrl: Type.Optional(Type.String()), - gatewayToken: Type.Optional(Type.String()), - timeoutMs: Type.Optional(Type.Number()), - includeDisabled: Type.Optional(Type.Boolean()), - job: Type.Optional(Type.Object({}, { additionalProperties: true })), - jobId: Type.Optional(Type.String()), - id: Type.Optional(Type.String()), - patch: Type.Optional(Type.Object({}, { additionalProperties: true })), - text: Type.Optional(Type.String()), - mode: optionalStringEnum(CRON_WAKE_MODES), - runMode: optionalStringEnum(CRON_RUN_MODES), - contextMessages: Type.Optional( - Type.Number({ minimum: 0, maximum: REMINDER_CONTEXT_MESSAGES_MAX }), - ), -}); +const CronToolSchema = Type.Object( + { + action: stringEnum(CRON_ACTIONS), + gatewayUrl: Type.Optional(Type.String()), + gatewayToken: Type.Optional(Type.String()), + timeoutMs: Type.Optional(Type.Number()), + includeDisabled: Type.Optional(Type.Boolean()), + job: Type.Optional(Type.Object({}, { additionalProperties: true })), + jobId: Type.Optional(Type.String()), + id: Type.Optional(Type.String()), + patch: Type.Optional(Type.Object({}, { additionalProperties: true })), + text: Type.Optional(Type.String()), + mode: optionalStringEnum(CRON_WAKE_MODES), + runMode: optionalStringEnum(CRON_RUN_MODES), + contextMessages: Type.Optional( + Type.Number({ minimum: 0, maximum: REMINDER_CONTEXT_MESSAGES_MAX }), + ), + }, + { additionalProperties: true }, +); type CronToolOptions = { agentSessionKey?: string; @@ -435,6 +438,42 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con if (!id) { throw new Error("jobId required (id accepted for backward compatibility)"); } + + // Flat-params recovery for patch + if ( + !params.patch || + (typeof params.patch === "object" && + params.patch !== null && + Object.keys(params.patch as Record).length === 0) + ) { + const PATCH_KEYS: ReadonlySet = new Set([ + "name", + "schedule", + "payload", + "delivery", + "enabled", + "description", + "deleteAfterRun", + "agentId", + "sessionKey", + "sessionTarget", + "wakeMode", + "failureAlert", + "allowUnsafeExternalContent", + ]); + const synthetic: Record = {}; + let found = false; + for (const key of Object.keys(params)) { + if (PATCH_KEYS.has(key) && params[key] !== undefined) { + synthetic[key] = params[key]; + found = true; + } + } + if (found) { + params.patch = synthetic; + } + } + if (!params.patch || typeof params.patch !== "object") { throw new Error("patch required"); }