mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
refactor(cron): centralize automation mutation options (#128373)
* refactor(cron): centralize automation mutation options * chore(cron): keep release notes out of root changelog * test(node-host): synchronize MCP refresh notifications explicitly * test(node-host): preserve refresh synchronization tuple types
This commit is contained in:
committed by
GitHub
parent
0f57d3f797
commit
102f18446f
@@ -75,6 +75,8 @@ Manage automations with the `openclaw automations` CLI; `openclaw cron` remains
|
||||
| `on-exit` | `--on-exit` | Fire once when a watched command exits (event trigger; survives turn teardown; optional `--on-exit-cwd`) |
|
||||
| `stream` | `--stream-command` | Fire from batched lines produced by a supervised long-lived command |
|
||||
|
||||
These schedule flags work with both `openclaw automations add` and `openclaw automations edit <job-id>`. For example, `openclaw automations edit <job-id> --on-exit "./watch.sh" --on-exit-cwd /srv/app` converts an existing job to an exit-triggered schedule.
|
||||
|
||||
Timestamps without a timezone are treated as UTC. Add `--tz America/New_York` to interpret an offset-less `--at` datetime, or to evaluate a cron expression, in that IANA timezone. Cron expressions without `--tz` use the Gateway host timezone. `--tz` is not valid with `--every` or `--on-exit`.
|
||||
|
||||
Recurring top-of-hour expressions (minute `0` with a wildcard hour field) are automatically staggered by up to 5 minutes to reduce load spikes. Use `--exact` to force precise timing, or `--stagger 30s` for an explicit window (cron schedules only).
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import type { Command } from "commander";
|
||||
import { theme } from "../../../packages/terminal-core/src/theme.js";
|
||||
import { THINKING_LEVELS_HELP } from "../../auto-reply/thinking.shared.js";
|
||||
import type { CronJob } from "../../cron/types.js";
|
||||
import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.js";
|
||||
import { sanitizeAgentId } from "../../routing/session-key.js";
|
||||
@@ -15,9 +14,9 @@ import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||
import { listCronJobsFromGateway } from "./list-jobs.js";
|
||||
import { createCronOutputCommand } from "./output-mode.js";
|
||||
import { registerCronMutationOptions } from "./register.cron-options.js";
|
||||
import { resolveCronCreateScheduleFromArgs } from "./schedule-options.js";
|
||||
import {
|
||||
getCronChannelOptions,
|
||||
coerceCronDeliveryPreviews,
|
||||
enrichCronJsonWithStatus,
|
||||
handleCronCliError,
|
||||
@@ -84,83 +83,15 @@ export function registerCronListCommand(cron: Command) {
|
||||
|
||||
export function registerCronAddCommand(cron: Command) {
|
||||
addGatewayClientOptions(
|
||||
createCronOutputCommand(cron, "add")
|
||||
.description("Add an automation")
|
||||
.argument("[scheduleOrName]", "Schedule string, or job name when using --at/--every/--cron")
|
||||
.argument("[message]", "Agent message when using a positional schedule")
|
||||
.option("--name <name>", "Job name")
|
||||
registerCronMutationOptions(
|
||||
createCronOutputCommand(cron, "add")
|
||||
.description("Add an automation")
|
||||
.argument("[scheduleOrName]", "Schedule string, or job name when using --at/--every/--cron")
|
||||
.argument("[message]", "Agent message when using a positional schedule"),
|
||||
"add",
|
||||
)
|
||||
.option("--declaration-key <key>", "Idempotent declaration identity key")
|
||||
.option("--display-name <name>", "Human-readable declarative job label")
|
||||
.option("--description <text>", "Optional description")
|
||||
.option("--disabled", "Create job disabled", false)
|
||||
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
|
||||
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
|
||||
.option("--agent <id>", "Agent id for this job")
|
||||
.option("--session <target>", "Session target (main|isolated|current|session:<id>)")
|
||||
.option("--session-key <key>", "Session key for job routing (e.g. agent:my-agent:my-session)")
|
||||
.option("--wake <mode>", "Wake mode (now|next-heartbeat)", "now")
|
||||
.option(
|
||||
"--at <when>",
|
||||
"Run once at time (ISO with offset, or +duration). Use --tz for offset-less datetimes",
|
||||
)
|
||||
.option("--every <duration>", "Run every duration (e.g. 10m, 1h)")
|
||||
.option("--pacing-min <duration>", "Minimum delay accepted from a dynamic next check")
|
||||
.option("--pacing-max <duration>", "Maximum delay accepted from a dynamic next check")
|
||||
.option("--cron <expr>", "Cron expression (5-field or 6-field with seconds)")
|
||||
.option(
|
||||
"--on-exit <shell>",
|
||||
"Fire once when this watched command exits (event trigger; survives turn teardown)",
|
||||
)
|
||||
.option("--on-exit-cwd <path>", "Working directory for the --on-exit watched command")
|
||||
.option("--stream-command <json>", "Stream source argv as a JSON array of strings")
|
||||
.option("--stream-cwd <path>", "Working directory for the stream source")
|
||||
.option("--stream-mode <mode>", "Stream line selection mode (line|match)")
|
||||
.option("--stream-match <regex>", "Regex source required for stream match mode")
|
||||
.option("--stream-batch-ms <n>", "Quiet-window batch delay in milliseconds")
|
||||
.option("--stream-max-batch-bytes <n>", "Maximum UTF-8 bytes per stream batch")
|
||||
.option(
|
||||
"--tz <iana>",
|
||||
"Timezone for cron expressions (IANA; cron default: Gateway host local timezone)",
|
||||
"",
|
||||
)
|
||||
.option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)")
|
||||
.option("--exact", "Disable cron staggering (set stagger to 0)", false)
|
||||
.option("--trigger-script <path|->", "Condition script file, or - for stdin")
|
||||
.option("--trigger-once", "Disable after the first successful triggered run", false)
|
||||
.option("--system-event <text>", "System event payload (main session)")
|
||||
.option("--message <text>", "Agent message payload")
|
||||
.option("--script <file|->", "Headless script payload file, or - for stdin")
|
||||
.option("--script-timeout-seconds <n>", "Script wall-clock timeout seconds")
|
||||
.option("--script-tool-budget <n>", "Maximum script tool calls")
|
||||
.option("--command <shell>", "Command payload run as sh -lc <shell> on the Gateway")
|
||||
.option("--command-argv <json>", "Command payload argv as JSON array of strings")
|
||||
.option("--command-cwd <path>", "Working directory for command payloads")
|
||||
.option(
|
||||
"--command-env <KEY=VALUE>",
|
||||
"Environment override for command payloads (repeatable)",
|
||||
(value: string, previous: string[] | undefined) => [...(previous ?? []), value],
|
||||
)
|
||||
.option("--command-input <text>", "stdin for command payloads")
|
||||
.option("--thinking <level>", `Thinking level for agent jobs (${THINKING_LEVELS_HELP})`)
|
||||
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
|
||||
.option("--fallbacks <list>", "Fallback model list for agent jobs")
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent or command jobs")
|
||||
.option("--no-output-timeout-seconds <n>", "No-output timeout seconds for command jobs")
|
||||
.option("--output-max-bytes <n>", "Maximum captured stdout/stderr bytes for command jobs")
|
||||
.option("--light-context", "Use lightweight bootstrap context for agent jobs", false)
|
||||
.option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)")
|
||||
.option("--announce", "Fallback-deliver final text to a chat", false)
|
||||
.option("--deliver", "Deprecated (use --announce). Fallback-delivers final text to a chat.")
|
||||
.option("--no-deliver", "Disable runner fallback delivery")
|
||||
.option("--webhook <url>", "POST the finished payload to a webhook URL")
|
||||
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`, "last")
|
||||
.option(
|
||||
"--to <dest>",
|
||||
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
||||
)
|
||||
.option("--thread-id <id>", "Telegram forum topic thread id")
|
||||
.option("--account <id>", "Channel account id for delivery (multi-account setups)")
|
||||
.option("--best-effort-deliver", "Do not fail the job if delivery fails", false)
|
||||
.action(
|
||||
async (
|
||||
nameArg: string | undefined,
|
||||
@@ -176,23 +107,7 @@ export function registerCronAddCommand(cron: Command) {
|
||||
typeof opts.onExit === "string" ||
|
||||
typeof opts.streamCommand === "string";
|
||||
const positionalSchedule = hasScheduleFlag ? undefined : nameArg;
|
||||
const schedule = resolveCronCreateScheduleFromArgs({
|
||||
at: opts.at,
|
||||
cron: opts.cron,
|
||||
every: opts.every,
|
||||
onExit: opts.onExit,
|
||||
onExitCwd: opts.onExitCwd,
|
||||
streamCommand: opts.streamCommand,
|
||||
streamCwd: opts.streamCwd,
|
||||
streamMode: opts.streamMode,
|
||||
streamMatch: opts.streamMatch,
|
||||
streamBatchMs: opts.streamBatchMs,
|
||||
streamMaxBatchBytes: opts.streamMaxBatchBytes,
|
||||
exact: opts.exact,
|
||||
positionalSchedule,
|
||||
stagger: opts.stagger,
|
||||
tz: opts.tz,
|
||||
});
|
||||
const schedule = resolveCronCreateScheduleFromArgs({ ...opts, positionalSchedule });
|
||||
|
||||
const wakeMode = normalizeOptionalString(opts.wake) ?? "now";
|
||||
if (wakeMode !== "now" && wakeMode !== "next-heartbeat") {
|
||||
|
||||
@@ -55,6 +55,9 @@ describe("cron edit command", () => {
|
||||
expect(help).toContain("--best-effort-deliver");
|
||||
expect(help).toContain("--display-name <name>");
|
||||
expect(help).toContain("--clear-display-name");
|
||||
expect(help).toContain("--on-exit <shell>");
|
||||
expect(help).toContain("--on-exit-cwd <path>");
|
||||
expect(help).toContain("main|isolated|current|session:<id>");
|
||||
expect(help).toMatch(/also\s+implies --announce when used alone/);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
normalizeOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import type { Command } from "commander";
|
||||
import { THINKING_LEVELS_HELP } from "../../auto-reply/thinking.shared.js";
|
||||
import type { CronJob } from "../../cron/types.js";
|
||||
import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.js";
|
||||
import { danger } from "../../globals.js";
|
||||
@@ -21,6 +20,7 @@ import { parseDurationMs } from "../parse-duration.js";
|
||||
import { isUnknownCronGetMethodError, listCronJobsFromGateway } from "./list-jobs.js";
|
||||
import { createCronOutputCommand } from "./output-mode.js";
|
||||
import { resolveCronEditPayloadDeliveryPatch } from "./register.cron-edit-options.js";
|
||||
import { registerCronMutationOptions } from "./register.cron-options.js";
|
||||
import {
|
||||
applyExistingCronSchedulePatch,
|
||||
applyExistingStreamSchedulePatch,
|
||||
@@ -57,98 +57,36 @@ async function readCronJobForEdit(opts: GatewayRpcOpts, id: string): Promise<Cro
|
||||
|
||||
export function registerCronEditCommand(cron: Command) {
|
||||
addGatewayClientOptions(
|
||||
createCronOutputCommand(cron, "edit")
|
||||
.description("Edit an automation (patch fields)")
|
||||
.argument("<id>", "Job id")
|
||||
.option("--name <name>", "Set name")
|
||||
.option("--display-name <name>", "Set human-readable display name")
|
||||
registerCronMutationOptions(
|
||||
createCronOutputCommand(cron, "edit")
|
||||
.description("Edit an automation (patch fields)")
|
||||
.argument("<id>", "Job id"),
|
||||
"edit",
|
||||
)
|
||||
.option("--clear-display-name", "Restore the stable name in list and detail views", false)
|
||||
.option("--description <text>", "Set description")
|
||||
.option("--enable", "Enable job", false)
|
||||
.option("--disable", "Disable job", false)
|
||||
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
|
||||
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
|
||||
.option("--session <target>", "Session target (main|isolated)")
|
||||
.option("--agent <id>", "Set agent id")
|
||||
.option("--clear-agent", "Unset agent and use default", false)
|
||||
.option("--session-key <key>", "Set session key for job routing")
|
||||
.option("--clear-session-key", "Unset session key", false)
|
||||
.option("--wake <mode>", "Wake mode (now|next-heartbeat)")
|
||||
.option("--at <when>", "Set one-shot time (ISO, offset-less uses --tz) or duration like 20m")
|
||||
.option("--every <duration>", "Set interval duration like 10m")
|
||||
.option("--pacing-min <duration>", "Set minimum delay for a dynamic next check")
|
||||
.option("--pacing-max <duration>", "Set maximum delay for a dynamic next check")
|
||||
.option("--clear-pacing", "Remove dynamic-cadence bounds", false)
|
||||
.option("--cron <expr>", "Set cron expression")
|
||||
.option("--stream-command <json>", "Set stream source argv as a JSON array of strings")
|
||||
.option("--stream-cwd <path>", "Set stream source working directory")
|
||||
.option("--stream-mode <mode>", "Set stream selection mode (line|match)")
|
||||
.option("--stream-match <regex>", "Set stream match regex source")
|
||||
.option("--stream-batch-ms <n>", "Set stream quiet-window delay in milliseconds")
|
||||
.option("--stream-max-batch-bytes <n>", "Set maximum UTF-8 bytes per stream batch")
|
||||
.option(
|
||||
"--tz <iana>",
|
||||
"Timezone for cron expressions (IANA; cron default: Gateway host local timezone)",
|
||||
)
|
||||
.option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)")
|
||||
.option("--exact", "Disable cron staggering (set stagger to 0)")
|
||||
.option("--trigger-script <path|->", "Set condition script from file, or - for stdin")
|
||||
.option("--trigger-once", "Disable after the first successful triggered run", false)
|
||||
.option("--clear-trigger", "Remove the condition trigger", false)
|
||||
.option("--system-event <text>", "Set systemEvent payload")
|
||||
.option("--message <text>", "Set agentTurn payload message")
|
||||
.option("--script <file|->", "Set headless script payload from file, or - for stdin")
|
||||
.option("--script-timeout-seconds <n>", "Set script wall-clock timeout seconds")
|
||||
.option("--script-tool-budget <n>", "Set maximum script tool calls")
|
||||
.option("--command <shell>", "Set command payload run as sh -lc <shell> on the Gateway")
|
||||
.option("--command-argv <json>", "Set command payload argv as JSON array of strings")
|
||||
.option("--command-cwd <path>", "Set command payload working directory")
|
||||
.option(
|
||||
"--command-env <KEY=VALUE>",
|
||||
"Set command payload environment overrides (repeatable)",
|
||||
(value: string, previous: string[] | undefined) => [...(previous ?? []), value],
|
||||
)
|
||||
.option("--command-input <text>", "Set command payload stdin")
|
||||
.option("--thinking <level>", `Thinking level for agent jobs (${THINKING_LEVELS_HELP})`)
|
||||
.option(
|
||||
"--clear-thinking",
|
||||
"Remove the per-job thinking override (restore normal cron thinking precedence)",
|
||||
false,
|
||||
)
|
||||
.option("--model <model>", "Model override for agent jobs")
|
||||
.option("--fallbacks <list>", "Fallback model list for agent jobs")
|
||||
.option("--clear-fallbacks", "Remove per-job fallback override", false)
|
||||
.option(
|
||||
"--clear-model",
|
||||
"Remove the per-job model override (restore normal cron model precedence)",
|
||||
false,
|
||||
)
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent or command jobs")
|
||||
.option("--no-output-timeout-seconds <n>", "No-output timeout seconds for command jobs")
|
||||
.option("--output-max-bytes <n>", "Maximum captured stdout/stderr bytes for command jobs")
|
||||
.option("--light-context", "Enable lightweight bootstrap context for agent jobs")
|
||||
.option("--no-light-context", "Disable lightweight bootstrap context for agent jobs")
|
||||
.option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)")
|
||||
.option("--clear-tools", "Remove tool allow-list (use all tools)", false)
|
||||
.option("--announce", "Fallback-deliver final text to a chat")
|
||||
.option("--deliver", "Deprecated (use --announce). Fallback-delivers final text to a chat.")
|
||||
.option("--no-deliver", "Disable runner fallback delivery")
|
||||
.option("--webhook <url>", "POST the finished payload to a webhook URL")
|
||||
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`)
|
||||
.option(
|
||||
"--to <dest>",
|
||||
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
||||
)
|
||||
.option("--thread-id <id>", "Telegram forum topic thread id")
|
||||
.option("--account <id>", "Channel account id for delivery (multi-account setups)")
|
||||
.option("--clear-channel", "Unset the delivery channel", false)
|
||||
.option("--clear-to", "Unset the delivery destination", false)
|
||||
.option("--clear-thread-id", "Unset the Telegram forum topic thread id", false)
|
||||
.option("--clear-account", "Unset the per-job delivery account override", false)
|
||||
.option(
|
||||
"--best-effort-deliver",
|
||||
"Do not fail job if delivery fails (also implies --announce when used alone)",
|
||||
)
|
||||
.option("--no-best-effort-deliver", "Fail job when delivery fails")
|
||||
.option("--failure-alert", "Enable failure alerts for this job")
|
||||
.option("--no-failure-alert", "Disable failure alerts for this job")
|
||||
@@ -369,20 +307,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
patch.trigger = { ...existing.trigger, once: true };
|
||||
}
|
||||
|
||||
const scheduleRequest = resolveCronEditScheduleRequest({
|
||||
at: opts.at,
|
||||
cron: opts.cron,
|
||||
every: opts.every,
|
||||
streamCommand: opts.streamCommand,
|
||||
streamCwd: opts.streamCwd,
|
||||
streamMode: opts.streamMode,
|
||||
streamMatch: opts.streamMatch,
|
||||
streamBatchMs: opts.streamBatchMs,
|
||||
streamMaxBatchBytes: opts.streamMaxBatchBytes,
|
||||
exact: opts.exact,
|
||||
stagger: opts.stagger,
|
||||
tz: opts.tz,
|
||||
});
|
||||
const scheduleRequest = resolveCronEditScheduleRequest(opts);
|
||||
if (scheduleRequest.kind === "direct") {
|
||||
if (scheduleRequest.schedule.kind === "stream") {
|
||||
const existing = await readExistingCronJob();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
|
||||
const callGatewayFromCli = vi.fn();
|
||||
|
||||
vi.mock("../gateway-rpc.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../gateway-rpc.js")>("../gateway-rpc.js");
|
||||
return {
|
||||
...actual,
|
||||
callGatewayFromCli: (...args: Parameters<typeof actual.callGatewayFromCli>) =>
|
||||
callGatewayFromCli(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const { registerCronAddCommand } = await import("./register.cron-add.js");
|
||||
const { registerCronEditCommand } = await import("./register.cron-edit.js");
|
||||
|
||||
function createMutationProgram(): Command {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerCronAddCommand(program);
|
||||
registerCronEditCommand(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
describe("shared automation mutation options", () => {
|
||||
beforeEach(() => {
|
||||
callGatewayFromCli.mockReset();
|
||||
callGatewayFromCli.mockResolvedValue({ ok: true });
|
||||
});
|
||||
|
||||
it("updates an existing automation to an exit-triggered schedule", async () => {
|
||||
await createMutationProgram().parseAsync(
|
||||
["edit", "job-1", "--on-exit", "./watch.sh", "--on-exit-cwd", "/repo"],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
expect(callGatewayFromCli).toHaveBeenCalledWith("cron.update", expect.anything(), {
|
||||
id: "job-1",
|
||||
patch: { schedule: { kind: "on-exit", command: "./watch.sh", cwd: "/repo" } },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[["--on-exit-cwd", "/repo"], "--on-exit-cwd requires --on-exit"],
|
||||
[["--on-exit", "./watch.sh", "--every", "5m"], "Choose at most one schedule change"],
|
||||
])("rejects invalid exit-triggered schedule options", async (args, message) => {
|
||||
const errorSpy = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {});
|
||||
const exitSpy = vi.spyOn(defaultRuntime, "exit").mockImplementation(() => undefined);
|
||||
try {
|
||||
await createMutationProgram().parseAsync(["edit", "job-1", ...args], { from: "user" });
|
||||
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(message));
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
expect(callGatewayFromCli).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
exitSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps creation defaults out of automation edit patches", () => {
|
||||
const program = createMutationProgram();
|
||||
const add = program.commands.find((command) => command.name() === "add")!;
|
||||
const edit = program.commands.find((command) => command.name() === "edit")!;
|
||||
const creationDefaults: Array<[string, string | boolean]> = [
|
||||
["wake", "now"],
|
||||
["tz", ""],
|
||||
["exact", false],
|
||||
["lightContext", false],
|
||||
["announce", false],
|
||||
["channel", "last"],
|
||||
["bestEffortDeliver", false],
|
||||
];
|
||||
|
||||
for (const [name, value] of creationDefaults) {
|
||||
expect(add.getOptionValue(name)).toBe(value);
|
||||
expect(edit.getOptionValue(name)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Command } from "commander";
|
||||
import { THINKING_LEVELS_HELP } from "../../auto-reply/thinking.shared.js";
|
||||
import { getCronChannelOptions } from "./shared.js";
|
||||
|
||||
export function registerCronMutationOptions(command: Command, mode: "add" | "edit"): Command {
|
||||
const create = mode === "add";
|
||||
return command
|
||||
.option("--name <name>", "Job name")
|
||||
.option("--display-name <name>", "Human-readable job label")
|
||||
.option("--description <text>", "Job description")
|
||||
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
|
||||
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
|
||||
.option("--agent <id>", "Agent id for this job")
|
||||
.option("--session <target>", "Session target (main|isolated|current|session:<id>)")
|
||||
.option("--session-key <key>", "Session key for job routing")
|
||||
.option("--wake <mode>", "Wake mode (now|next-heartbeat)", create ? "now" : undefined)
|
||||
.option("--at <when>", "One-shot time (ISO, offset-less uses --tz) or duration like 20m")
|
||||
.option("--every <duration>", "Interval duration (e.g. 10m, 1h)")
|
||||
.option("--pacing-min <duration>", "Minimum delay for a dynamic next check")
|
||||
.option("--pacing-max <duration>", "Maximum delay for a dynamic next check")
|
||||
.option("--cron <expr>", "Cron expression (5-field or 6-field with seconds)")
|
||||
.option("--on-exit <shell>", "Fire once when the watched command exits")
|
||||
.option("--on-exit-cwd <path>", "Working directory for the --on-exit watched command")
|
||||
.option("--stream-command <json>", "Stream source argv as a JSON array of strings")
|
||||
.option("--stream-cwd <path>", "Working directory for the stream source")
|
||||
.option("--stream-mode <mode>", "Stream line selection mode (line|match)")
|
||||
.option("--stream-match <regex>", "Regex source required for stream match mode")
|
||||
.option("--stream-batch-ms <n>", "Quiet-window batch delay in milliseconds")
|
||||
.option("--stream-max-batch-bytes <n>", "Maximum UTF-8 bytes per stream batch")
|
||||
.option(
|
||||
"--tz <iana>",
|
||||
"Timezone for cron expressions (IANA; cron default: Gateway host local timezone)",
|
||||
create ? "" : undefined,
|
||||
)
|
||||
.option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)")
|
||||
.option("--exact", "Disable cron staggering (set stagger to 0)", create ? false : undefined)
|
||||
.option("--trigger-script <path|->", "Condition script file, or - for stdin")
|
||||
.option("--trigger-once", "Disable after the first successful triggered run", false)
|
||||
.option("--system-event <text>", "System event payload (main session)")
|
||||
.option("--message <text>", "Agent message payload")
|
||||
.option("--script <file|->", "Headless script payload file, or - for stdin")
|
||||
.option("--script-timeout-seconds <n>", "Script wall-clock timeout seconds")
|
||||
.option("--script-tool-budget <n>", "Maximum script tool calls")
|
||||
.option("--command <shell>", "Command payload run as sh -lc <shell> on the Gateway")
|
||||
.option("--command-argv <json>", "Command payload argv as JSON array of strings")
|
||||
.option("--command-cwd <path>", "Working directory for command payloads")
|
||||
.option(
|
||||
"--command-env <KEY=VALUE>",
|
||||
"Environment override for command payloads (repeatable)",
|
||||
(value: string, previous: string[] | undefined) => [...(previous ?? []), value],
|
||||
)
|
||||
.option("--command-input <text>", "stdin for command payloads")
|
||||
.option("--thinking <level>", `Thinking level for agent jobs (${THINKING_LEVELS_HELP})`)
|
||||
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
|
||||
.option("--fallbacks <list>", "Fallback model list for agent jobs")
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent or command jobs")
|
||||
.option("--no-output-timeout-seconds <n>", "No-output timeout seconds for command jobs")
|
||||
.option("--output-max-bytes <n>", "Maximum captured stdout/stderr bytes for command jobs")
|
||||
.option(
|
||||
"--light-context",
|
||||
"Use lightweight bootstrap context for agent jobs",
|
||||
create ? false : undefined,
|
||||
)
|
||||
.option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)")
|
||||
.option("--announce", "Fallback-deliver final text to a chat", create ? false : undefined)
|
||||
.option("--deliver", "Deprecated (use --announce). Fallback-delivers final text to a chat.")
|
||||
.option("--no-deliver", "Disable runner fallback delivery")
|
||||
.option("--webhook <url>", "POST the finished payload to a webhook URL")
|
||||
.option(
|
||||
"--channel <channel>",
|
||||
`Delivery channel (${getCronChannelOptions()})`,
|
||||
create ? "last" : undefined,
|
||||
)
|
||||
.option("--to <dest>", "Delivery destination (E.164, Telegram chatId, or Discord channel/user)")
|
||||
.option("--thread-id <id>", "Telegram forum topic thread id")
|
||||
.option("--account <id>", "Channel account id for delivery (multi-account setups)")
|
||||
.option(
|
||||
"--best-effort-deliver",
|
||||
create
|
||||
? "Do not fail the job if delivery fails"
|
||||
: "Do not fail job if delivery fails (also implies --announce when used alone)",
|
||||
create ? false : undefined,
|
||||
);
|
||||
}
|
||||
@@ -274,6 +274,7 @@ describe("node host MCP live lifecycle", () => {
|
||||
let maxActiveLists = 0;
|
||||
let listCount = 0;
|
||||
const pending: Array<(value: { tools: Tool[] }) => void> = [];
|
||||
const refreshStarted = [createDeferred(), createDeferred()] as const;
|
||||
const client = createClient({
|
||||
list: async () => {
|
||||
listCount += 1;
|
||||
@@ -285,6 +286,7 @@ describe("node host MCP live lifecycle", () => {
|
||||
try {
|
||||
return await new Promise<{ tools: Tool[] }>((resolve) => {
|
||||
pending.push(resolve);
|
||||
refreshStarted[listCount - 2]?.resolve();
|
||||
});
|
||||
} finally {
|
||||
activeLists -= 1;
|
||||
@@ -304,13 +306,15 @@ describe("node host MCP live lifecycle", () => {
|
||||
);
|
||||
|
||||
notifyToolsChanged?.();
|
||||
await vi.waitFor(() => expect(activeLists).toBe(1));
|
||||
await refreshStarted[0].promise;
|
||||
expect(activeLists).toBe(1);
|
||||
for (let index = 0; index < 20; index += 1) {
|
||||
notifyToolsChanged?.();
|
||||
}
|
||||
expect(client.request).toHaveBeenCalledTimes(2);
|
||||
pending.shift()?.({ tools: [tool("middle")] });
|
||||
await vi.waitFor(() => expect(client.request).toHaveBeenCalledTimes(3));
|
||||
await refreshStarted[1].promise;
|
||||
expect(client.request).toHaveBeenCalledTimes(3);
|
||||
expect(maxActiveLists).toBe(1);
|
||||
pending.shift()?.({ tools: [tool("final")] });
|
||||
await vi.waitFor(() =>
|
||||
|
||||
Reference in New Issue
Block a user