From a98a64649ec00ffdd359126bcf8cd39e1e0a2582 Mon Sep 17 00:00:00 2001 From: SunnyShu Date: Mon, 6 Jul 2026 21:48:00 +0800 Subject: [PATCH] fix(cron): accept null fallbacks in update patch payload (#100707) (#100801) * fix(cron): accept null fallbacks in cron update patch payload createCronPatchObjectSchema passes nullableStringArraySchema for model and toolsAllow, but fallbacks was still hardcoded as a non-nullable array in cronPayloadObjectSchema, rejecting null clears before they reach the Gateway service layer. Switch fallbacks to a parameter so the patch context can pass the nullable variant, matching the existing pattern for model and toolsAllow. Fixes #100707 Co-Authored-By: Claude * test(cron): cover model+fallbacks null clear in patch schema test --------- Co-authored-by: Claude --- src/agents/tools/cron-tool.schema.test.ts | 15 +++++++++++++++ src/agents/tools/cron-tool.ts | 10 ++++++++-- .../codex-dynamic-tools.discord-group.json | 17 ++++++++++++----- .../codex-dynamic-tools.heartbeat-turn.json | 17 ++++++++++++----- .../codex-dynamic-tools.telegram-direct.json | 17 ++++++++++++----- .../discord-group-codex-message-tool.md | 8 ++++---- .../telegram-direct-codex-message-tool.md | 8 ++++---- .../telegram-heartbeat-codex-tool.md | 8 ++++---- 8 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/agents/tools/cron-tool.schema.test.ts b/src/agents/tools/cron-tool.schema.test.ts index 37dc48a0f827..3ba2cbfbb855 100644 --- a/src/agents/tools/cron-tool.schema.test.ts +++ b/src/agents/tools/cron-tool.schema.test.ts @@ -231,6 +231,21 @@ describe("createCronToolSchema", () => { ).toBe(true); }); + it("accepts payload.model and payload.fallbacks null in patch (clear-to-inherit)", () => { + expect( + Value.Check(createCronToolSchema(), { + action: "update", + jobId: "job-1", + patch: { + payload: { + model: null, + fallbacks: null, + }, + }, + }), + ).toBe(true); + }); + it("job.agentId and job.sessionKey project to plain string type for OpenAPI 3.0 compat", () => { const root = providerSchemaRecord.properties as | Record }> diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index c0291bb1f6ae..12adeb21e797 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -109,7 +109,11 @@ function failureDestinationModeSchema(params: { nullableClears: boolean }) { return Type.Optional(Type.Union(variants)); } -function cronPayloadObjectSchema(params: { model: TSchema; toolsAllow: TSchema }) { +function cronPayloadObjectSchema(params: { + model: TSchema; + toolsAllow: TSchema; + fallbacks: TSchema; +}) { return Type.Object( { kind: optionalStringEnum(CRON_PAYLOAD_KINDS, { description: "Payload kind" }), @@ -120,7 +124,7 @@ function cronPayloadObjectSchema(params: { model: TSchema; toolsAllow: TSchema } timeoutSeconds: optionalFiniteNumberSchema({ minimum: 0 }), lightContext: Type.Optional(Type.Boolean()), allowUnsafeExternalContent: Type.Optional(Type.Boolean()), - fallbacks: Type.Optional(Type.Array(Type.String(), { description: "Fallback models" })), + fallbacks: params.fallbacks, toolsAllow: params.toolsAllow, }, { additionalProperties: true }, @@ -161,6 +165,7 @@ function createCronPayloadSchema(): TSchema { cronPayloadObjectSchema({ model: Type.Optional(Type.String({ description: "Model override" })), toolsAllow: Type.Optional(Type.Array(Type.String(), { description: "Allowed tools" })), + fallbacks: Type.Optional(Type.Array(Type.String(), { description: "Fallback models" })), }), ); } @@ -313,6 +318,7 @@ function createCronPatchObjectSchema(): TSchema { cronPayloadObjectSchema({ model: nullableStringSchema("Model override, or null to clear"), toolsAllow: nullableStringArraySchema("Allowed tool ids, or null to clear"), + fallbacks: nullableStringArraySchema("Fallback models, or null to clear"), }), ), delivery: createCronDeliveryPatchSchema(), diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json index b9b3e0bceedd..78fbc8c11a43 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json @@ -904,11 +904,18 @@ "type": "boolean" }, "fallbacks": { - "description": "Fallback models", - "items": { - "type": "string" - }, - "type": "array" + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Fallback models, or null to clear" }, "kind": { "description": "Payload kind", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json index f85dea39d366..66fe9aecac5b 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json @@ -900,11 +900,18 @@ "type": "boolean" }, "fallbacks": { - "description": "Fallback models", - "items": { - "type": "string" - }, - "type": "array" + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Fallback models, or null to clear" }, "kind": { "description": "Payload kind", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json index d6709ac3689b..3fabe33e4529 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json @@ -900,11 +900,18 @@ "type": "boolean" }, "fallbacks": { - "description": "Fallback models", - "items": { - "type": "string" - }, - "type": "array" + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Fallback models, or null to clear" }, "kind": { "description": "Payload kind", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 9aaac848da41..5a7c77c49da7 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 51703, - "roughTokens": 12926 + "chars": 51940, + "roughTokens": 12985 }, "openClawDeveloperInstructions": { "chars": 3045, @@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6893 }, "totalWithDynamicToolsJson": { - "chars": 79275, - "roughTokens": 19819 + "chars": 79512, + "roughTokens": 19878 }, "userInputText": { "chars": 1442, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index aa361a8daade..cf464652a7a4 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 51392, - "roughTokens": 12848 + "chars": 51629, + "roughTokens": 12908 }, "openClawDeveloperInstructions": { "chars": 1936, @@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6513 }, "totalWithDynamicToolsJson": { - "chars": 77446, - "roughTokens": 19362 + "chars": 77683, + "roughTokens": 19421 }, "userInputText": { "chars": 1033, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index f91241c54fc4..2deb8476ec0f 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -228,8 +228,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 52682, - "roughTokens": 13171 + "chars": 52919, + "roughTokens": 13230 }, "openClawDeveloperInstructions": { "chars": 1955, @@ -240,8 +240,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6749 }, "totalWithDynamicToolsJson": { - "chars": 79679, - "roughTokens": 19920 + "chars": 79916, + "roughTokens": 19979 }, "userInputText": { "chars": 1271,