mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(cron): stop advertising trigger-gated fields when cron triggers are disabled (#122052)
* fix(cron): stop advertising trigger-gated fields when cron triggers are disabled When cron.triggers.enabled is off (the documented default posture), the cron tool still advertised job.trigger/patch.trigger, schedule.kind "stream" with its stream-only fields, and payload.kind "script" — all of which the scheduler unconditionally rejects. Schema-eager models attach junk triggers to plain reminder requests, get rejected, and retry-loop without ever creating the job. Gate those surfaces on the resolved config so they are only advertised when the runtime can actually accept them, and swap the trigger guidance for a short unavailable notice so the model tells the user instead of polling or silently degrading the request. Config-less construction paths keep the full surface. Related: #119455 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cron): restore config type import and regenerate schema contracts Repairs rebase fallout and CI-contract consequences of the trigger gating: - Re-import OpenClawConfig from config/types.openclaw.js; the rebase crossed #121768 which removed the re-export from config/config.js, stranding resolveCronTriggersEnabled's signature. - Guard the destructured tool in the MCP gating test for noUncheckedIndexedAccess. - Collapse the hookContext merge object to one line: the added config pass-through tipped openclaw-tools.ts over the max-lines ratchet (701/700). - Regenerate prompt snapshots (cron description narrowed under triggers-disabled harness config; ~3k chars per prompt) and the plugin-sdk API baseline (CronToolOptions gained the optional config member, visible via codex-mcp-projection). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(cron): align schema contracts with current main --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
This commit is contained in:
@@ -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"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"d5a6edec36b8675110181c771b79fac5753d242ce8adc58022743eb0184c34f2","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
{"contentHash":"0cff92037a9d807ad0301e14dd319645f9e27336e2d07aa4613bce129f86a827","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"74e044d5e8a93fd319609f0d130eea0c1e636111e7c2597cbc3c98b847515bd3","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
{"contentHash":"2534a0f3e5bc7c6f7dd08cdc732f705660dbd5a71d992b81406ec0926054ed56","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"d10f59babb0166e4db0a66e801ca674ad23b158737939d9213c3bc6258f2cc5c","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
{"contentHash":"475a9e86021703425ca3a33e602011d8e26ab7525f082b4daa35bf9ef40effb1","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"dcbaf714f293fbe933587344f798c0d86bfd65dc74e05e552c08666695da5125","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
{"contentHash":"16f5001d40fb5196f43834fff175a4e20cd7fe16c3020828dea46bf3fe7a9312","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"db4f872ae8ea2b1c36068c4fa4d0550e49550f34d31cbb4898533cdde9f4d103","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
{"contentHash":"e0c691c494cf0d17c251a21fecc4c856bce1c76ea647ed8ba62287d000145671","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"9a9729aaa9a46400464a7c2b5240541a9f379271b863b25e1c4dd1c3b7508e73","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
{"contentHash":"80d1dfc109596a642c6e54cc9a0755f6d9824067cfa069742ba7c94bb790b6b6","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"4d4adfe9f53f025c8bb22ee1a1dff2e224dae6bb25158ac1ac991cf7ebad249b","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
{"contentHash":"7f10ab95ec2d34e84789f8f298ddc1abbd1c5bb2fe804b84af469c7707ec8824","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"61e6ba4e3564811059b3d27ecfee1df8b63f151a8a43bab7190336b02f76889d","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
{"contentHash":"77a500390fb4b5d465334b23fabb2f77c1ca9178a6e095a2340a4a950c7fece0","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"647420ef973a58b7078e974eba6ab85b6d2df96f28e07c3bce108c0da3f52c68","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
{"contentHash":"6697592146ce483645a562acf9a105b57541733ca631ccba638164256ac501a9","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"d2d5d792a37398f060d0e069077d794d7e2d91a23fb0a4849ec66b8591f86daa","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
{"contentHash":"519c2334419a6eb156e96ae99fca2a77821f60be3d8714a17062493591df50e4","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"ae862c22e6d6f96239ea1666faa208c5445aef89b6f3bc4425e3a259c3c6cb2b","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
{"contentHash":"1ada23abeeeec02cbd00d3274fb591b85269bd3098d911a390b363b1ba5eca5f","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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:<id>",
|
||||
}),
|
||||
),
|
||||
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(
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:<key>" = 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:"<dur>" 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<string, unknown>;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, unknown> } };
|
||||
};
|
||||
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,
|
||||
|
||||
@@ -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 }],
|
||||
});
|
||||
});
|
||||
|
||||
+3
-63
@@ -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:<key>\" = 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:\"<dur>\" 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:<key>\" = 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:\"<dur>\" 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:<id>",
|
||||
"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"],
|
||||
|
||||
Vendored
+4
-4
@@ -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,
|
||||
|
||||
test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md
Vendored
+4
-4
@@ -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,
|
||||
|
||||
Vendored
+4
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user