fix(cron): recover flat patch params for update action and fix schema (openclaw#23221) thanks @charojo

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: charojo <4084797+charojo@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
charo
2026-03-01 20:50:51 -05:00
committed by GitHub
parent a779c2ca6a
commit 757e09fe43
3 changed files with 103 additions and 17 deletions

View File

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

View File

@@ -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 });
});
});

View File

@@ -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<string, unknown>).length === 0)
) {
const PATCH_KEYS: ReadonlySet<string> = new Set([
"name",
"schedule",
"payload",
"delivery",
"enabled",
"description",
"deleteAfterRun",
"agentId",
"sessionKey",
"sessionTarget",
"wakeMode",
"failureAlert",
"allowUnsafeExternalContent",
]);
const synthetic: Record<string, unknown> = {};
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");
}