diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index 6c32621c92fc..9f8856ff20e7 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"6d42269794e0473fdae832d6c1af49854a822438f4b3b0943fb855846cbd5f64","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"51617df7eeada155ae66bf04a3a30b8622708f48e6b0274ec2b831ff583f8e58","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index 2e4af18aaedb..b4b368b4b584 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"d5a6edec36b8675110181c771b79fac5753d242ce8adc58022743eb0184c34f2","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"0cff92037a9d807ad0301e14dd319645f9e27336e2d07aa4613bce129f86a827","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index 1e2b0707b36b..6f8031ecc445 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"74e044d5e8a93fd319609f0d130eea0c1e636111e7c2597cbc3c98b847515bd3","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"2534a0f3e5bc7c6f7dd08cdc732f705660dbd5a71d992b81406ec0926054ed56","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index 8995ff213a4b..8fd6bbdd5462 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"176592119d65008bfa8d1b465670c9556b0275d432e738353ea5a25d0a61985d","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"6df8cf7345a116398af3b08f0715e5cfc5a0386b3a0c357ba5140eff626f0191","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index 19efe706c718..15d5b67384f0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"d10f59babb0166e4db0a66e801ca674ad23b158737939d9213c3bc6258f2cc5c","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"475a9e86021703425ca3a33e602011d8e26ab7525f082b4daa35bf9ef40effb1","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index 59eb4ae86570..72b1b6ef2354 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"dcbaf714f293fbe933587344f798c0d86bfd65dc74e05e552c08666695da5125","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"16f5001d40fb5196f43834fff175a4e20cd7fe16c3020828dea46bf3fe7a9312","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index c0816cae3df3..9a4904fb9a02 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"5e92fe659c6fa93639b6e197bca678ec533c190208c65eee3ef4cb24f3929848","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"c1a6a7c97f0a333a6b617eeb22017fb33bf2d57e4d884fce73c7849999b57f72","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index 9abaf9a92cb9..bc1018ea434a 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"db4f872ae8ea2b1c36068c4fa4d0550e49550f34d31cbb4898533cdde9f4d103","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"e0c691c494cf0d17c251a21fecc4c856bce1c76ea647ed8ba62287d000145671","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 459a86f0f791..78d6fac81856 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"9a9729aaa9a46400464a7c2b5240541a9f379271b863b25e1c4dd1c3b7508e73","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"80d1dfc109596a642c6e54cc9a0755f6d9824067cfa069742ba7c94bb790b6b6","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index 2cbbcfca9353..79de2feb2392 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"0006fe56489e1a95488e436e5b2d881c353003ba24163aa5620f8b52c87c0d99","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"09f4cd3417e8a942d4636d4ac3297f54eaeeefc4a24b94dcfca9d23ae76cb463","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index c1ff4fe82a8d..3facc71815b1 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"4d4adfe9f53f025c8bb22ee1a1dff2e224dae6bb25158ac1ac991cf7ebad249b","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"7f10ab95ec2d34e84789f8f298ddc1abbd1c5bb2fe804b84af469c7707ec8824","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index c90c87fe5b5f..85d389c3bb40 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"61e6ba4e3564811059b3d27ecfee1df8b63f151a8a43bab7190336b02f76889d","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"77a500390fb4b5d465334b23fabb2f77c1ca9178a6e095a2340a4a950c7fece0","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index 1489aa426f84..0362e34c821d 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"647420ef973a58b7078e974eba6ab85b6d2df96f28e07c3bce108c0da3f52c68","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"6697592146ce483645a562acf9a105b57541733ca631ccba638164256ac501a9","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index 8d27f3dac668..679d4cf04c46 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"61e41f87d2a11f6c85ff712b500fc2cf50b974ee2f701e9c69c1693dc206e15d","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"12a68d5eb0af0b8026102f6faf695e5146e997bb3ce17882e576c8b63eb08c0c","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index e24f107c3e89..ae8829a6ed31 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"d2d5d792a37398f060d0e069077d794d7e2d91a23fb0a4849ec66b8591f86daa","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"519c2334419a6eb156e96ae99fca2a77821f60be3d8714a17062493591df50e4","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index 000f288bb039..d7bd97998ad6 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"ae862c22e6d6f96239ea1666faa208c5445aef89b6f3bc4425e3a259c3c6cb2b","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"1ada23abeeeec02cbd00d3274fb591b85269bd3098d911a390b363b1ba5eca5f","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 153868710153..46b7c4208b9e 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -490,6 +490,7 @@ export function createOpenClawTools( // Use the durable runSessionKey; cleanup-retired policy keys leave cron jobs dangling. agentSessionKey: options?.runSessionKey ?? options?.agentSessionKey, agentAccountId: gatewayCallerAccountId, + config: options?.config, currentDeliveryContext: { channel: options?.agentChannel, to: options?.currentChannelId ?? options?.agentTo, @@ -757,10 +758,7 @@ export function createOpenClawTools( ...(options?.currentChannelId ? { channelId: options.currentChannelId } : {}), loopDetection: resolveToolLoopDetectionConfig({ cfg: resolvedConfig, agentId: hookAgentId }), }; - const hookContext = { - ...defaultHookContext, - ...options?.beforeToolCallHookContext, - }; + const hookContext = { ...defaultHookContext, ...options?.beforeToolCallHookContext }; options?.recordToolPrepStage?.("openclaw-tools:tool-hooks"); return allTools .map((tool) => diff --git a/src/agents/tools/cron-tool-schema.ts b/src/agents/tools/cron-tool-schema.ts index bf81bcf83149..3776ef69c48f 100644 --- a/src/agents/tools/cron-tool-schema.ts +++ b/src/agents/tools/cron-tool-schema.ts @@ -35,11 +35,27 @@ const CRON_ACTIONS = [ ] as const; const CRON_SCHEDULE_KINDS = ["at", "every", "cron", "stream"] as const; +// Stream schedules, script payloads, and condition triggers all require +// cron.triggers.enabled; when it is off the scheduler rejects them, so the +// model-facing schema must not advertise them. +const CRON_SCHEDULE_KINDS_TRIGGERS_DISABLED = ["at", "every", "cron"] as const; const CRON_WAKE_MODES = ["now", "next-heartbeat"] as const; const CRON_PAYLOAD_KINDS = ["systemEvent", "agentTurn", "script"] as const; +const CRON_PAYLOAD_KINDS_TRIGGERS_DISABLED = ["systemEvent", "agentTurn"] as const; const CRON_DELIVERY_MODES = ["none", "announce", "webhook"] as const; const CRON_RUN_MODES = ["due", "force"] as const; +type CronToolSchemaOptions = { + /** + * Whether cron.triggers.enabled is on for this deployment. When false, the + * trigger-gated surfaces (job trigger, script payloads, stream + * schedules) are omitted from the advertised schema so models cannot be + * tempted into calls the scheduler always rejects. Defaults to true so + * config-less callers keep the full surface. + */ + triggersEnabled?: boolean; +}; + function nullableStringSchema(description: string) { return Type.Optional(Type.Union([Type.String(), Type.Null()], { description })); } @@ -51,11 +67,14 @@ function deliveryStringSchema(description: string) { return nullableStringSchema(`${description}, or null to clear`); } -function createCronScheduleSchema(): TSchema { +function createCronScheduleSchema(params: { triggersEnabled: boolean }): TSchema { return Type.Optional( Type.Object( { - kind: optionalStringEnum(CRON_SCHEDULE_KINDS, { description: "Schedule kind" }), + kind: optionalStringEnum( + params.triggersEnabled ? CRON_SCHEDULE_KINDS : CRON_SCHEDULE_KINDS_TRIGGERS_DISABLED, + { description: "Schedule kind" }, + ), at: Type.Optional(Type.String({ description: "ISO-8601 time (kind=at)" })), everyMs: optionalPositiveIntegerSchema({ description: "Interval ms (kind=every)", @@ -81,17 +100,24 @@ function createCronScheduleSchema(): TSchema { description: "Jitter ms (kind=cron)", maximum: MAX_DATE_TIMESTAMP_MS, }), - command: Type.Optional( - Type.Array(Type.String({ minLength: 1 }), { - minItems: 1, - description: "Supervised source argv (kind=stream; requires cron.triggers.enabled)", - }), - ), - cwd: Type.Optional(Type.String({ description: "Working directory (kind=stream)" })), - mode: optionalStringEnum(["line", "match"] as const), - match: Type.Optional(Type.String({ description: "Regex source (stream match mode)" })), - batchMs: optionalNonNegativeIntegerSchema(), - maxBatchBytes: optionalNonNegativeIntegerSchema(), + ...(params.triggersEnabled + ? { + command: Type.Optional( + Type.Array(Type.String({ minLength: 1 }), { + minItems: 1, + description: + "Supervised source argv (kind=stream; requires cron.triggers.enabled)", + }), + ), + cwd: Type.Optional(Type.String({ description: "Working directory (kind=stream)" })), + mode: optionalStringEnum(["line", "match"] as const), + match: Type.Optional( + Type.String({ description: "Regex source (stream match mode)" }), + ), + batchMs: optionalNonNegativeIntegerSchema(), + maxBatchBytes: optionalNonNegativeIntegerSchema(), + } + : {}), }, { additionalProperties: true }, ), @@ -122,18 +148,31 @@ export function assertCronPacingInput(value: unknown): void { parseCronPacingBounds(value as CronPacing); } -function createCronPayloadSchema(): TSchema { +function createCronPayloadSchema(params: { triggersEnabled: boolean }): TSchema { return Type.Optional( Type.Object( { - kind: optionalStringEnum(CRON_PAYLOAD_KINDS, { description: "Payload kind" }), + kind: optionalStringEnum( + params.triggersEnabled ? CRON_PAYLOAD_KINDS : CRON_PAYLOAD_KINDS_TRIGGERS_DISABLED, + { description: "Payload kind" }, + ), text: Type.Optional(Type.String({ description: "systemEvent text" })), message: Type.Optional(Type.String({ description: "agentTurn prompt" })), - script: Type.Optional(Type.String({ description: "Headless code-mode script" })), + ...(params.triggersEnabled + ? { + script: Type.Optional(Type.String({ description: "Headless code-mode script" })), + } + : {}), model: nullableStringSchema("Model override, or null to clear"), thinking: Type.Optional(Type.String({ description: "Thinking override" })), timeoutSeconds: optionalFiniteNumberSchema({ minimum: 0 }), - toolBudget: optionalPositiveIntegerSchema({ description: "Maximum script tool calls" }), + ...(params.triggersEnabled + ? { + toolBudget: optionalPositiveIntegerSchema({ + description: "Maximum script tool calls", + }), + } + : {}), lightContext: Type.Optional( Type.Boolean({ description: "Lightweight bootstrap context (skip full workspace context)", @@ -237,7 +276,7 @@ function createCronFailureAlertSchema(): TSchema { ); } -function createCronJobObjectSchema(): TSchema { +function createCronJobObjectSchema(params: { triggersEnabled: boolean }): TSchema { return Type.Optional( Type.Object( { @@ -263,16 +302,16 @@ function createCronJobObjectSchema(): TSchema { { additionalProperties: false }, ), ), - schedule: createCronScheduleSchema(), + schedule: createCronScheduleSchema({ triggersEnabled: params.triggersEnabled }), pacing: createCronPacingSchema(), - trigger: createCronTriggerSchema(), + ...(params.triggersEnabled ? { trigger: createCronTriggerSchema() } : {}), sessionTarget: Type.Optional( Type.String({ description: "main | isolated | current (agentTurn default) | session:", }), ), wakeMode: optionalStringEnum(CRON_WAKE_MODES, { description: "Wake timing" }), - payload: createCronPayloadSchema(), + payload: createCronPayloadSchema({ triggersEnabled: params.triggersEnabled }), delivery: createCronDeliverySchema(), agentId: nullableStringSchema("Agent id, or null to clear it"), description: Type.Optional(Type.String({ description: "Human description" })), @@ -291,7 +330,8 @@ function createCronJobObjectSchema(): TSchema { } // Flattened schema: runtime validates per-action requirements. -export function createCronToolSchema(): TSchema { +export function createCronToolSchema(options?: CronToolSchemaOptions): TSchema { + const triggersEnabled = options?.triggersEnabled !== false; return Type.Object( { action: stringEnum(CRON_ACTIONS), @@ -304,7 +344,7 @@ export function createCronToolSchema(): TSchema { offset: optionalNonNegativeIntegerSchema({ description: 'Job offset for action="list"; use nextOffset to load the next page', }), - job: createCronJobObjectSchema(), + job: createCronJobObjectSchema({ triggersEnabled }), jobId: Type.Optional(Type.String()), id: Type.Optional(Type.String()), in: Type.Optional( diff --git a/src/agents/tools/cron-tool.schema.test.ts b/src/agents/tools/cron-tool.schema.test.ts index 8e1203161dcb..7395a621d190 100644 --- a/src/agents/tools/cron-tool.schema.test.ts +++ b/src/agents/tools/cron-tool.schema.test.ts @@ -7,6 +7,7 @@ import { MAX_DATE_TIMESTAMP_MS } from "@openclaw/normalization-core/number-coerc // validation compatibility for cron jobs. import { Value } from "typebox/value"; import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { createCronTool } from "./cron-tool.js"; /** Unwraps nullable anyOf unions to their object variant so paths can descend. */ @@ -472,3 +473,73 @@ describe("createCronToolSchema", () => { expect(json).not.toMatch(/"not"\s*:\s*\{/); }); }); + +describe("createCronToolSchema with cron triggers disabled", () => { + const triggersDisabledConfig = { + cron: { enabled: true, triggers: { enabled: false } }, + } as OpenClawConfig; + const tool = createCronTool({ config: triggersDisabledConfig }); + const schemaRecord = tool.parameters as unknown as Record; + + it("omits trigger from job", () => { + expect(keysAt(schemaRecord, "job")).not.toContain("trigger"); + }); + + it("omits stream schedules from kind enums and drops stream-only fields", () => { + expect(propertyAt(schemaRecord, "job.schedule.kind")?.enum).toEqual(["at", "every", "cron"]); + const scheduleKeys = keysAt(schemaRecord, "job.schedule"); + for (const streamField of ["command", "cwd", "mode", "match", "batchMs", "maxBatchBytes"]) { + expect(scheduleKeys).not.toContain(streamField); + } + }); + + it("omits script payloads from kind enums and drops script-only fields", () => { + expect(propertyAt(schemaRecord, "job.payload.kind")?.enum).toEqual([ + "systemEvent", + "agentTurn", + ]); + const payloadKeys = keysAt(schemaRecord, "job.payload"); + expect(payloadKeys).not.toContain("script"); + expect(payloadKeys).not.toContain("toolBudget"); + }); + + it("tells the model triggers are unavailable instead of documenting them", () => { + expect(tool.description).toContain("TRIGGERS DISABLED"); + expect(tool.description).not.toContain("TRIGGER (condition watcher"); + expect(tool.description).not.toContain('kind:"stream"'); + expect(tool.description).not.toContain('kind:"script"'); + expect(tool.description).not.toContain("Silent watcher"); + expect(tool.description).not.toContain("event watchers"); + expect(tool.description).toContain("say it is unsupported"); + }); + + it("keeps the full surface when no config is provided", () => { + const configlessSchema = createCronTool().parameters as unknown as Record; + expect(keysAt(configlessSchema, "job")).toContain("trigger"); + expect(propertyAt(configlessSchema, "job.schedule.kind")?.enum).toContain("stream"); + }); + + it("gates the surface when config omits cron.triggers entirely (disabled default)", () => { + const defaultPostureSchema = createCronTool({ + config: { cron: { enabled: true } } as OpenClawConfig, + }).parameters as unknown as Record; + expect(keysAt(defaultPostureSchema, "job")).not.toContain("trigger"); + expect(propertyAt(defaultPostureSchema, "job.schedule.kind")?.enum).toEqual([ + "at", + "every", + "cron", + ]); + }); + + it("still validates a plain reminder add call", () => { + expect( + Value.Check(tool.parameters, { + action: "add", + job: { + schedule: { kind: "cron", expr: "0 9 * * *", tz: "America/New_York" }, + payload: { kind: "agentTurn", message: "Morning summary" }, + }, + }), + ).toBe(true); + }); +}); diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 6fc2c3c28b74..8f046ee944fd 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -6,6 +6,7 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { parseDurationMs } from "../../cli/parse-duration.js"; import { getRuntimeConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveCronCreationDelivery } from "../../cron/delivery-context.js"; import { assertCronDeliveryInputNonBlankFields } from "../../cron/delivery-target-validation.js"; import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js"; @@ -166,23 +167,30 @@ function isOlderGatewayWithoutCompactCronList(error: unknown): boolean { ); } -export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): AnyAgentTool { - const callGateway = deps?.callGatewayTool ?? callGatewayTool; - const tool: AnyAgentTool = { - label: "Automations", - name: AUTOMATIONS_TOOL_NAME, - displaySummary: CRON_TOOL_DISPLAY_SUMMARY, - description: `Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer. +function buildCronToolDescription(params: { triggersEnabled: boolean }): string { + const addFields = params.triggersEnabled + ? "{name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}" + : "{name?,schedule,payload,sessionTarget?,pacing?,delivery?,enabled?}"; + const streamScheduleLine = params.triggersEnabled + ? '\n- {kind:"stream",command:[argv],mode?:"line"|"match",match?}: fires on supervised process output; needs cron.triggers.enabled.' + : ""; + const scriptPayloadLine = params.triggersEnabled + ? '\n- script {kind:"script",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.' + : ""; + const triggerSection = params.triggersEnabled + ? `TRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call("exec",{command:"..."}).` + : `TRIGGERS DISABLED (cron.triggers.enabled=false): condition triggers, script payloads, and stream schedules are unavailable here. Omit trigger; use plain time-based schedules. If the user asks for a conditional watcher, say it is unsupported — never model-poll instead, and never silently create an unconditional job in its place.`; + const silentWatcherCue = params.triggersEnabled ? ' Silent watcher=>mode:"none".' : ""; + return `Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work${params.triggersEnabled ? ", event watchers" : ""}. Never exec sleep/poll as timer. ACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId job (partial: only supplied fields change; null clears) | remove jobId | run jobId (runMode "force"=now) | runs jobId = history | next_check in:"30m" (own paced run only) | wake text mode?:"now"|"next-heartbeat"(default) nudges a caller-owned lane (sessionKey/agentId to pick another). -ADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload. +ADD: ${addFields}. Required: schedule+payload. SCHEDULE: - {kind:"at",at:"ISO-8601"} one-shot; no tz=UTC; auto-deletes after run. - {kind:"every",everyMs}. -- {kind:"cron",expr,tz?:"IANA"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:"0 18 * * *",tz:"Asia/Shanghai"}. -- {kind:"stream",command:[argv],mode?:"line"|"match",match?}: fires on supervised process output; needs cron.triggers.enabled. +- {kind:"cron",expr,tz?:"IANA"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:"0 18 * * *",tz:"Asia/Shanghai"}.${streamScheduleLine} TARGET+PAYLOAD: - "current" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/"continue later"/loop = at|every + agentTurn + current. @@ -190,17 +198,37 @@ TARGET+PAYLOAD: - "main" = heartbeat lane; payload {kind:"systemEvent",text} (systemEvent default target). - "session:" = named session. - agentTurn {kind:"agentTurn",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none. -- Inherited configured MCP authority includes only model-callable tools; interactive app-view-only capabilities are excluded from headless jobs. -- script {kind:"script",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled. +- Inherited configured MCP authority includes only model-callable tools; interactive app-view-only capabilities are excluded from headless jobs.${scriptPayloadLine} PACED LOOP: recurring job + pacing{min?,max?} durations ("15m","4h"; at least one). Inside its run, job calls next_check in:"" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet. -TRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call("exec",{command:"..."}). +${triggerSection} -DELIVERY {mode:"none"|"announce"|"webhook",channel?,to?,threadId?,bestEffort?,completionDestination?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:"none". webhook posts finished-run event to URL in \`to\`. To keep announce delivery and also POST completion, use mode:"announce" with completionDestination:{mode:"webhook",to:"https://..."}. +DELIVERY {mode:"none"|"announce"|"webhook",channel?,to?,threadId?,bestEffort?,completionDestination?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run).${silentWatcherCue} webhook posts finished-run event to URL in \`to\`. To keep announce delivery and also POST completion, use mode:"announce" with completionDestination:{mode:"webhook",to:"https://..."}. -Job wakeMode (main jobs): "now"(default)|"next-heartbeat". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.`, - parameters: createCronToolSchema(), +Job wakeMode (main jobs): "now"(default)|"next-heartbeat". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.`; +} + +// Trigger-gated surfaces stay advertised for config-less callers; only an +// explicit runtime config with cron.triggers.enabled !== true narrows the +// model-facing surface, matching the scheduler's own gate in +// cron/service/jobs-validation.ts. +function resolveCronTriggersEnabled(config?: OpenClawConfig): boolean { + if (!config) { + return true; + } + return config.cron?.triggers?.enabled === true; +} + +export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): AnyAgentTool { + const callGateway = deps?.callGatewayTool ?? callGatewayTool; + const triggersEnabled = resolveCronTriggersEnabled(opts?.config); + const tool: AnyAgentTool = { + label: "Automations", + name: AUTOMATIONS_TOOL_NAME, + displaySummary: CRON_TOOL_DISPLAY_SUMMARY, + description: buildCronToolDescription({ triggersEnabled }), + parameters: createCronToolSchema({ triggersEnabled }), execute: async (_toolCallId, args, operationSignal) => { operationSignal?.throwIfAborted(); const params = args as Record; diff --git a/src/agents/tools/cron-tool.types.ts b/src/agents/tools/cron-tool.types.ts index d26a78a4db03..21f00130f545 100644 --- a/src/agents/tools/cron-tool.types.ts +++ b/src/agents/tools/cron-tool.types.ts @@ -1,6 +1,7 @@ +// Cron tool type declarations shared with the cron tool implementation. +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { CronRuntimeAuthority } from "../../cron/runtime-authority.js"; import type { CronCreatorAuthorityGrant } from "../../gateway/cron-creator-authority-grant.js"; -// Cron tool type declarations shared with the cron tool implementation. import type { DeliveryContext } from "../../utils/delivery-context.shared.js"; import type { callGatewayTool } from "./gateway.js"; @@ -39,6 +40,13 @@ export type CronToolOptions = { agentSessionKey?: string; /** Authenticated source account; authority must not be inferred from delivery. */ agentAccountId?: string; + /** + * Resolved config for the calling context. Shapes the advertised schema and + * description: when cron.triggers.enabled is off, trigger-gated surfaces + * (trigger, script payloads, stream schedules) are not advertised. Omitting + * config keeps the full surface for config-less callers. + */ + config?: OpenClawConfig; currentDeliveryContext?: DeliveryContext; /** * Effective tool surface visible to the caller that created or edited a cron job. diff --git a/src/mcp/openclaw-tools-serve.test.ts b/src/mcp/openclaw-tools-serve.test.ts index e7bc614d4f3c..cd11bb460d07 100644 --- a/src/mcp/openclaw-tools-serve.test.ts +++ b/src/mcp/openclaw-tools-serve.test.ts @@ -31,6 +31,26 @@ describe("OpenClaw tools MCP server", () => { expect(listed.tools.map((tool) => tool.name)).toContain("automations"); }); + it("gates cron trigger surfaces by the host config", () => { + const jobKeys = (config: unknown) => { + const [tool] = resolveOpenClawToolsForMcp({ + agentSessionKey: "agent:worker:main", + config: config as never, + }); + if (!tool) { + throw new Error("expected the automations tool to be resolved"); + } + const parameters = tool.parameters as unknown as { + properties: { job: { properties: Record } }; + }; + return Object.keys(parameters.properties.job.properties); + }; + + expect(jobKeys({ cron: { triggers: { enabled: false } } })).not.toContain("trigger"); + expect(jobKeys({ cron: {} })).not.toContain("trigger"); + expect(jobKeys({ cron: { triggers: { enabled: true } } })).toContain("trigger"); + }); + it("requires the managed bridge to pass a real agent session key", () => { expect(() => resolveOpenClawToolsForMcp({ agentSessionKey: "" })).toThrow( OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV, diff --git a/src/mcp/openclaw-tools-serve.ts b/src/mcp/openclaw-tools-serve.ts index eb92b0b87dd5..f49355961000 100644 --- a/src/mcp/openclaw-tools-serve.ts +++ b/src/mcp/openclaw-tools-serve.ts @@ -11,6 +11,8 @@ import type { AnyAgentTool } from "../agents/tools/common.js"; import { createCronTool } from "../agents/tools/cron-tool.js"; import { createSystemAgentTool } from "../agents/tools/system-agent-tool.js"; import type { SystemAgentToolOptions } from "../agents/tools/system-agent-tool.js"; +import { getRuntimeConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV, @@ -42,6 +44,7 @@ export function resolveOpenClawToolsForMcp( agentSessionKey?: string; tools?: OpenClawToolsMcpToolId[]; systemAgentSurface?: SystemAgentToolOptions["surface"]; + config?: OpenClawConfig; } = {}, ): AnyAgentTool[] { const selection = params.tools ?? resolveOpenClawToolsMcpToolSelection(); @@ -60,6 +63,9 @@ export function resolveOpenClawToolsForMcp( } return createCronTool({ agentSessionKey, + // Same host-config resolution as plugin-tools-serve: the advertised cron + // surface must reflect this deployment's cron.triggers.enabled gate. + config: params.config ?? getRuntimeConfig(), creatorToolAllowlist: [{ name: AUTOMATIONS_TOOL_NAME }], }); }); 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 3d117900c0f2..9de153575e39 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 @@ -261,7 +261,7 @@ "tools": [ { "deferLoading": true, - "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId job (partial: only supplied fields change; null clears) | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,trigger?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n- {kind:\"stream\",command:[argv],mode?:\"line\"|\"match\",match?}: fires on supervised process output; needs cron.triggers.enabled.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- Inherited configured MCP authority includes only model-callable tools; interactive app-view-only capabilities are excluded from headless jobs.\n- script {kind:\"script\",script,timeoutSeconds?,toolBudget?}: main|isolated only; needs cron.triggers.enabled.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGER (condition watcher on every/cron): {script,once?}; needs cron.triggers.enabled — if off, say so; never model-poll instead. Quiet headless check, no model; 30s/5 tool calls/16KB state. Read frozen trigger.state, return json({fire,message?,state?}) with NEW state; dedupe via state, never memory. fire:false saves state only. fire:true runs payload; message is that run's entire context — self-contained. Fire on failures/timeouts too; success-only watchers look healthy when broken. Script stays read-only; actions belong in payload. once:true disables after first fire. Code Mode: await tools.call(\"exec\",{command:\"...\"}).\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?,completionDestination?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). Silent watcher=>mode:\"none\". webhook posts finished-run event to URL in `to`. To keep announce delivery and also POST completion, use mode:\"announce\" with completionDestination:{mode:\"webhook\",to:\"https://...\"}.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", + "description": "Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work. Never exec sleep/poll as timer.\n\nACTIONS: status | list [includeDisabled,limit?,offset?] (use nextOffset for the next page) | get jobId | add job | update jobId job (partial: only supplied fields change; null clears) | remove jobId | run jobId (runMode \"force\"=now) | runs jobId = history | next_check in:\"30m\" (own paced run only) | wake text mode?:\"now\"|\"next-heartbeat\"(default) nudges a caller-owned lane (sessionKey/agentId to pick another).\n\nADD: {name?,schedule,payload,sessionTarget?,pacing?,delivery?,enabled?}. Required: schedule+payload.\n\nSCHEDULE:\n- {kind:\"at\",at:\"ISO-8601\"} one-shot; no tz=UTC; auto-deletes after run.\n- {kind:\"every\",everyMs}.\n- {kind:\"cron\",expr,tz?:\"IANA\"}: expr is wall time in tz; never pre-convert to UTC; no tz=gateway host local. 18:00 Shanghai => {expr:\"0 18 * * *\",tz:\"Asia/Shanghai\"}.\n\nTARGET+PAYLOAD:\n- \"current\" (agentTurn default) = this conversation: run carries this chat's context, result lands here. Self-wakeup/\"continue later\"/loop = at|every + agentTurn + current.\n- \"isolated\" = fresh detached session (shows in `openclaw tasks`); standalone background work.\n- \"main\" = heartbeat lane; payload {kind:\"systemEvent\",text} (systemEvent default target).\n- \"session:\" = named session.\n- agentTurn {kind:\"agentTurn\",message,model?,thinking?,timeoutSeconds?}; timeoutSeconds 0=none.\n- Inherited configured MCP authority includes only model-callable tools; interactive app-view-only capabilities are excluded from headless jobs.\n\nPACED LOOP: recurring job + pacing{min?,max?} durations (\"15m\",\"4h\"; at least one). Inside its run, job calls next_check in:\"\" to set the next delay (clamped to bounds, measured from run end; failed runs keep normal backoff). Adaptive polling: tighten when active, back off when quiet.\n\nTRIGGERS DISABLED (cron.triggers.enabled=false): condition triggers, script payloads, and stream schedules are unavailable here. Omit trigger; use plain time-based schedules. If the user asks for a conditional watcher, say it is unsupported — never model-poll instead, and never silently create an unconditional job in its place.\n\nDELIVERY {mode:\"none\"|\"announce\"|\"webhook\",channel?,to?,threadId?,bestEffort?,completionDestination?}: where detached run output goes. Omitted=announce (current=>this chat; isolated=>last route; set channel/to for a specific chat — no messaging tool inside the run). webhook posts finished-run event to URL in `to`. To keep announce delivery and also POST completion, use mode:\"announce\" with completionDestination:{mode:\"webhook\",to:\"https://...\"}.\n\nJob wakeMode (main jobs): \"now\"(default)|\"next-heartbeat\". Restricted automation-run sessions: self status/list/get/runs/remove + own next_check only. failureAlert {...}|false disables. jobId canonical (id=compat). contextMessages 0-10 embeds recent chat lines into reminder text.", "inputSchema": { "additionalProperties": true, "properties": { @@ -593,7 +593,7 @@ }, "kind": { "description": "Payload kind", - "enum": ["systemEvent", "agentTurn", "script"], + "enum": ["systemEvent", "agentTurn"], "type": "string" }, "lightContext": { @@ -615,10 +615,6 @@ ], "description": "Model override, or null to clear" }, - "script": { - "description": "Headless code-mode script", - "type": "string" - }, "text": { "description": "systemEvent text", "type": "string" @@ -631,11 +627,6 @@ "minimum": 0, "type": "number" }, - "toolBudget": { - "description": "Maximum script tool calls", - "minimum": 1, - "type": "integer" - }, "toolsAllow": { "anyOf": [ { @@ -666,23 +657,6 @@ "description": "ISO-8601 time (kind=at)", "type": "string" }, - "batchMs": { - "minimum": 0, - "type": "integer" - }, - "command": { - "description": "Supervised source argv (kind=stream; requires cron.triggers.enabled)", - "items": { - "minLength": 1, - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "cwd": { - "description": "Working directory (kind=stream)", - "type": "string" - }, "everyMs": { "description": "Interval ms (kind=every)", "maximum": 8640000000000000, @@ -695,19 +669,7 @@ }, "kind": { "description": "Schedule kind", - "enum": ["at", "every", "cron", "stream"], - "type": "string" - }, - "match": { - "description": "Regex source (stream match mode)", - "type": "string" - }, - "maxBatchBytes": { - "minimum": 0, - "type": "integer" - }, - "mode": { - "enum": ["line", "match"], + "enum": ["at", "every", "cron"], "type": "string" }, "staggerMs": { @@ -738,28 +700,6 @@ "description": "main | isolated | current (agentTurn default) | session:", "type": "string" }, - "trigger": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "once": { - "type": "boolean" - }, - "script": { - "maxLength": 65536, - "minLength": 1, - "type": "string" - } - }, - "required": ["script"], - "type": "object" - }, - { - "type": "null" - } - ] - }, "wakeMode": { "description": "Wake timing", "enum": ["now", "next-heartbeat"], 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 33bba283d4ff..35a7e804d73f 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": 52171, - "roughTokens": 13043 + "chars": 49173, + "roughTokens": 12294 }, "openClawDeveloperInstructions": { "chars": 4479, @@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 7216 }, "totalWithDynamicToolsJson": { - "chars": 81035, - "roughTokens": 20259 + "chars": 78037, + "roughTokens": 19510 }, "userInputText": { "chars": 1300, 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 d91fd4998a35..399dfef25d7b 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": 51863, - "roughTokens": 12966 + "chars": 48865, + "roughTokens": 12217 }, "openClawDeveloperInstructions": { "chars": 3370, @@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6846 }, "totalWithDynamicToolsJson": { - "chars": 79247, - "roughTokens": 19812 + "chars": 76249, + "roughTokens": 19063 }, "userInputText": { "chars": 929, 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 917906222826..450c8c3b2e6d 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 @@ -222,8 +222,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 53397, - "roughTokens": 13350 + "chars": 50399, + "roughTokens": 12600 }, "openClawDeveloperInstructions": { "chars": 3370, @@ -234,8 +234,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6950 }, "totalWithDynamicToolsJson": { - "chars": 81197, - "roughTokens": 20300 + "chars": 78199, + "roughTokens": 19550 }, "userInputText": { "chars": 1271,