From 9bb1e59447a5fdf2c0164672df6c748188b26442 Mon Sep 17 00:00:00 2001 From: "openclaw-clownfish[bot]" <280122609+openclaw-clownfish[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 01:32:27 -0700 Subject: [PATCH] fix(cron): preserve model overrides for text payloads (#73946) Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + src/cron/normalize.test.ts | 79 ++++++++++++++++++++++++++++++++++++++ src/cron/normalize.ts | 17 +++++--- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31bfc38efcd..a24797d21e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai - Control UI: fix Peak Error Hours showing incorrect hourly rates when the browser's timezone observes DST, by storing hourly message counts with UTC date keys and using DST-aware `Date.getHours()` for local conversion. Also extract `accumulateMessageCounts` helper to reduce duplicated daily/hourly aggregation logic. (#49396) Thanks @konanok. - iMessage: normalize known leading attributedBody corruption markers on sent-message echo text keys so delayed reflected echoes with U+FFFD/U+FFFE/U+FFFF/FEFF prefixes are dropped without collapsing interior text. Fixes #59973; carries forward #59980 and #62191. Thanks @neeravmakwana and @maguilar631697. - Security/audit: recognize dangerous node command IDs as valid `gateway.nodes.denyCommands` entries, so audit only warns on real typos or unsupported patterns. (#56923) Thanks @chziyue. +- Cron: treat implicit text payloads with agent-turn overrides as agent turns, preserving model overrides for scheduled text prompts instead of pruning them as system events. Fixes #28905. (#64060) Thanks @liaoandi. - Telegram/exec approvals: stop treating general Telegram chat allowlists and `defaultTo` routes as native exec approvers; Telegram now uses explicit `execApprovals.approvers` or owner identity from `commands.ownerAllowFrom`, matching the first-pairing owner bootstrap path. Thanks @pashpashpash. - Plugins/providers: keep Gateway startup primary-model discovery on metadata-only provider entries and reuse active non-speech capability providers even with explicit plugin entries, avoiding unnecessary provider registry loads during startup and media capability checks. Fixes #73729, #73835, and #73793; carries forward #73853 and #73794. Thanks @sg1416-zg, @brokemac79, and @poolside-ventures. - Chat commands: route sensitive group `/diagnostics` and `/export-trajectory` approvals and results to a private owner route, preferring same-surface DMs before falling back to the first configured owner route, so Discord group invocations can land in Telegram when that is the primary owner interface. Thanks @pashpashpash. diff --git a/src/cron/normalize.test.ts b/src/cron/normalize.test.ts index 7ba0fdc1763..c08fdcf6311 100644 --- a/src/cron/normalize.test.ts +++ b/src/cron/normalize.test.ts @@ -458,6 +458,51 @@ describe("normalizeCronJobCreate", () => { expect(validateCronAddParams(normalized)).toBe(true); }); + it("promotes implicit text payloads with agentTurn hints for create jobs", () => { + const normalized = normalizeCronJobCreate({ + name: "nested text model", + schedule: { kind: "every", everyMs: 60_000 }, + payload: { + text: " summarize issue status ", + model: " anthropic/claude-sonnet-4-6 ", + thinking: " high ", + }, + }) as unknown as Record; + + const payload = normalized.payload as Record; + expect(payload).toEqual({ + kind: "agentTurn", + message: "summarize issue status", + model: "anthropic/claude-sonnet-4-6", + thinking: "high", + }); + expect(normalized.sessionTarget).toBe("isolated"); + expect(validateCronAddParams(normalized)).toBe(true); + }); + + it("promotes legacy top-level text with agentTurn hints for create jobs", () => { + const normalized = normalizeCronJobCreate({ + name: "legacy text model", + schedule: { kind: "every", everyMs: 60_000 }, + text: " summarize issue status ", + model: " openrouter/deepseek/deepseek-r1 ", + fallbacks: [], + toolsAllow: [" read "], + }) as unknown as Record; + + const payload = normalized.payload as Record; + expect(payload).toEqual({ + kind: "agentTurn", + message: "summarize issue status", + model: "openrouter/deepseek/deepseek-r1", + fallbacks: [], + toolsAllow: ["read"], + }); + expect(normalized.text).toBeUndefined(); + expect(normalized.model).toBeUndefined(); + expect(validateCronAddParams(normalized)).toBe(true); + }); + it("preserves timeoutSeconds=0 for no-timeout agentTurn payloads", () => { const normalized = normalizeCronJobCreate({ name: "legacy no-timeout", @@ -678,6 +723,40 @@ describe("normalizeCronJobPatch", () => { expect(payload.model).toBe("anthropic/claude-sonnet-4-6"); }); + it("promotes implicit text payloads with agentTurn hints for patches", () => { + const normalized = normalizeCronJobPatch({ + payload: { + text: " summarize issue status ", + model: "anthropic/claude-sonnet-4-6", + }, + }) as unknown as Record; + + const payload = normalized.payload as Record; + expect(payload).toEqual({ + kind: "agentTurn", + message: "summarize issue status", + model: "anthropic/claude-sonnet-4-6", + }); + expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true); + }); + + it("promotes legacy top-level text with agentTurn hints for patches", () => { + const normalized = normalizeCronJobPatch({ + text: " summarize issue status ", + model: "openrouter/deepseek/deepseek-r1", + }) as unknown as Record; + + const payload = normalized.payload as Record; + expect(payload).toEqual({ + kind: "agentTurn", + message: "summarize issue status", + model: "openrouter/deepseek/deepseek-r1", + }); + expect(normalized.text).toBeUndefined(); + expect(normalized.model).toBeUndefined(); + expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true); + }); + it("infers agentTurn kind for lightContext-only payload patches", () => { const normalized = normalizeCronJobPatch({ payload: { diff --git a/src/cron/normalize.ts b/src/cron/normalize.ts index de9b80fcbbd..25094feae43 100644 --- a/src/cron/normalize.ts +++ b/src/cron/normalize.ts @@ -170,13 +170,17 @@ function coercePayload(payload: UnknownRecord) { next.kind = kindRaw; } if (!next.kind) { - const hasMessage = Boolean(normalizeOptionalString(next.message)); - const hasText = Boolean(normalizeOptionalString(next.text)); - if (hasMessage) { + const message = normalizeOptionalString(next.message); + const text = normalizeOptionalString(next.text); + const hasAgentTurnHint = hasAgentTurnPayloadHint(next); + if (message) { next.kind = "agentTurn"; - } else if (hasText) { + } else if (text && hasAgentTurnHint) { + next.kind = "agentTurn"; + next.message = text; + } else if (text) { next.kind = "systemEvent"; - } else if (hasAgentTurnPayloadHint(next)) { + } else if (hasAgentTurnHint) { // Accept partial agentTurn payload patches that only tweak agent-turn-only fields. next.kind = "agentTurn"; } @@ -311,6 +315,9 @@ function inferTopLevelPayload(next: UnknownRecord) { const text = normalizeOptionalString(next.text) ?? ""; if (text) { + if (hasAgentTurnPayloadHint(next)) { + return { kind: "agentTurn", message: text } satisfies UnknownRecord; + } return { kind: "systemEvent", text } satisfies UnknownRecord; }