diff --git a/src/cron/normalize.ts b/src/cron/normalize.ts index b1afdfaaa12..1446d99d935 100644 --- a/src/cron/normalize.ts +++ b/src/cron/normalize.ts @@ -101,7 +101,8 @@ function coercePayload(payload: UnknownRecord) { typeof next.model === "string" || typeof next.thinking === "string" || typeof next.timeoutSeconds === "number" || - typeof next.allowUnsafeExternalContent === "boolean"; + typeof next.allowUnsafeExternalContent === "boolean" || + Array.isArray(next.fallbacks); if (hasMessage) { next.kind = "agentTurn"; } else if (hasText) { diff --git a/src/cron/service.jobs.test.ts b/src/cron/service.jobs.test.ts index c514f7528ba..61b5a787e58 100644 --- a/src/cron/service.jobs.test.ts +++ b/src/cron/service.jobs.test.ts @@ -214,6 +214,53 @@ describe("applyJobPatch", () => { } }); + it("persists agentTurn payload.fallbacks updates when editing existing jobs", () => { + const job = createIsolatedAgentTurnJob("job-fallbacks", { + mode: "announce", + channel: "telegram", + }); + job.payload = { + kind: "agentTurn", + message: "do it", + fallbacks: ["anthropic/claude-sonnet-4-6"], + }; + + applyJobPatch(job, { + payload: { + kind: "agentTurn", + message: "do it", + fallbacks: ["openai/gpt-4o", "anthropic/claude-haiku-4-5"], + }, + }); + + expect(job.payload.kind).toBe("agentTurn"); + if (job.payload.kind === "agentTurn") { + expect(job.payload.fallbacks).toEqual(["openai/gpt-4o", "anthropic/claude-haiku-4-5"]); + } + }); + + it("applies payload.fallbacks when replacing payload kind via patch", () => { + const job = createIsolatedAgentTurnJob("job-fallbacks-switch", { + mode: "announce", + channel: "telegram", + }); + job.payload = { kind: "systemEvent", text: "ping" }; + + applyJobPatch(job, { + payload: { + kind: "agentTurn", + message: "do it", + fallbacks: ["anthropic/claude-sonnet-4-6"], + }, + }); + + const payload = job.payload as CronJob["payload"]; + expect(payload.kind).toBe("agentTurn"); + if (payload.kind === "agentTurn") { + expect(payload.fallbacks).toEqual(["anthropic/claude-sonnet-4-6"]); + } + }); + it("rejects webhook delivery without a valid http(s) target URL", () => { const expectedError = "cron webhook delivery requires delivery.to to be a valid http(s) URL"; const cases = [ diff --git a/src/cron/service/jobs.ts b/src/cron/service/jobs.ts index 542ba81053d..41ba1eeadca 100644 --- a/src/cron/service/jobs.ts +++ b/src/cron/service/jobs.ts @@ -704,6 +704,9 @@ function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronP if (typeof patch.bestEffortDeliver === "boolean") { next.bestEffortDeliver = patch.bestEffortDeliver; } + if (Array.isArray(patch.fallbacks)) { + next.fallbacks = patch.fallbacks; + } return next; } @@ -772,6 +775,7 @@ function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload { channel: patch.channel, to: patch.to, bestEffortDeliver: patch.bestEffortDeliver, + fallbacks: patch.fallbacks, }; }