feat(cron): enhance delivery modes and job configuration
- Updated isolated cron jobs to support new delivery modes: `announce` and `none`, improving output management. - Refactored job configuration to remove legacy fields and streamline delivery settings. - Enhanced the `CronJobEditor` UI to reflect changes in delivery options, including a new segmented control for delivery mode selection. - Updated documentation to clarify the new delivery configurations and their implications for job execution. - Improved tests to validate the new delivery behavior and ensure backward compatibility with legacy settings. This update provides users with greater flexibility in managing how isolated jobs deliver their outputs, enhancing overall usability and clarity in job configurations.
This commit is contained in:
committed by
Peter Steinberger
parent
ab9f06f4ff
commit
3f82daefd8
@@ -8,7 +8,7 @@ import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
||||
import {
|
||||
getCronChannelOptions,
|
||||
parseAtMs,
|
||||
parseAt,
|
||||
parseDurationMs,
|
||||
printCronList,
|
||||
warnIfCronSchedulerDisabled,
|
||||
@@ -82,24 +82,14 @@ export function registerCronAddCommand(cron: Command) {
|
||||
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||
.option("--announce", "Announce summary to a chat (subagent-style)", false)
|
||||
.option(
|
||||
"--deliver",
|
||||
"Deliver full output to a chat (required when using last-route delivery without --to)",
|
||||
)
|
||||
.option("--no-deliver", "Disable delivery and skip main-session summary")
|
||||
.option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.")
|
||||
.option("--no-deliver", "Disable announce delivery and skip main-session summary")
|
||||
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`, "last")
|
||||
.option(
|
||||
"--to <dest>",
|
||||
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
||||
)
|
||||
.option("--best-effort-deliver", "Do not fail the job if delivery fails", false)
|
||||
.option("--post-prefix <prefix>", "Prefix for main-session post", "Cron")
|
||||
.option(
|
||||
"--post-mode <mode>",
|
||||
"What to post back to main for isolated jobs (summary|full)",
|
||||
"summary",
|
||||
)
|
||||
.option("--post-max-chars <n>", "Max chars when --post-mode=full (default 8000)", "8000")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts: GatewayRpcOpts & Record<string, unknown>, cmd?: Command) => {
|
||||
try {
|
||||
@@ -112,11 +102,11 @@ export function registerCronAddCommand(cron: Command) {
|
||||
throw new Error("Choose exactly one schedule: --at, --every, or --cron");
|
||||
}
|
||||
if (at) {
|
||||
const atMs = parseAtMs(at);
|
||||
if (!atMs) {
|
||||
const atIso = parseAt(at);
|
||||
if (!atIso) {
|
||||
throw new Error("Invalid --at; use ISO time or duration like 20m");
|
||||
}
|
||||
return { kind: "at" as const, atMs };
|
||||
return { kind: "at" as const, at: atIso };
|
||||
}
|
||||
if (every) {
|
||||
const everyMs = parseDurationMs(every);
|
||||
@@ -143,12 +133,11 @@ export function registerCronAddCommand(cron: Command) {
|
||||
? sanitizeAgentId(opts.agent.trim())
|
||||
: undefined;
|
||||
|
||||
const hasAnnounce = Boolean(opts.announce);
|
||||
const hasDeliver = opts.deliver === true;
|
||||
const hasAnnounce = Boolean(opts.announce) || opts.deliver === true;
|
||||
const hasNoDeliver = opts.deliver === false;
|
||||
const deliveryFlagCount = [hasAnnounce, hasDeliver, hasNoDeliver].filter(Boolean).length;
|
||||
const deliveryFlagCount = [hasAnnounce, hasNoDeliver].filter(Boolean).length;
|
||||
if (deliveryFlagCount > 1) {
|
||||
throw new Error("Choose at most one of --announce, --deliver, or --no-deliver");
|
||||
throw new Error("Choose at most one of --announce or --no-deliver");
|
||||
}
|
||||
|
||||
const payload = (() => {
|
||||
@@ -203,56 +192,16 @@ export function registerCronAddCommand(cron: Command) {
|
||||
(opts.announce || typeof opts.deliver === "boolean") &&
|
||||
(sessionTarget !== "isolated" || payload.kind !== "agentTurn")
|
||||
) {
|
||||
throw new Error("--announce/--deliver/--no-deliver require --session isolated.");
|
||||
throw new Error("--announce/--no-deliver require --session isolated.");
|
||||
}
|
||||
|
||||
const hasLegacyPostConfig =
|
||||
optionSource("postPrefix") === "cli" ||
|
||||
optionSource("postMode") === "cli" ||
|
||||
optionSource("postMaxChars") === "cli";
|
||||
|
||||
if (
|
||||
hasLegacyPostConfig &&
|
||||
(sessionTarget !== "isolated" || payload.kind !== "agentTurn")
|
||||
) {
|
||||
throw new Error(
|
||||
"--post-prefix/--post-mode/--post-max-chars require --session isolated.",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasLegacyPostConfig && (hasAnnounce || hasDeliver || hasNoDeliver)) {
|
||||
throw new Error("Choose legacy main-summary options or a delivery mode (not both).");
|
||||
}
|
||||
|
||||
const isolation =
|
||||
sessionTarget === "isolated" && hasLegacyPostConfig
|
||||
? {
|
||||
postToMainPrefix:
|
||||
typeof opts.postPrefix === "string" && opts.postPrefix.trim()
|
||||
? opts.postPrefix.trim()
|
||||
: "Cron",
|
||||
postToMainMode:
|
||||
opts.postMode === "full" || opts.postMode === "summary"
|
||||
? opts.postMode
|
||||
: undefined,
|
||||
postToMainMaxChars:
|
||||
opts.postMode === "full" &&
|
||||
typeof opts.postMaxChars === "string" &&
|
||||
/^\d+$/.test(opts.postMaxChars)
|
||||
? Number.parseInt(opts.postMaxChars, 10)
|
||||
: undefined,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const deliveryMode =
|
||||
sessionTarget === "isolated" && payload.kind === "agentTurn" && !hasLegacyPostConfig
|
||||
sessionTarget === "isolated" && payload.kind === "agentTurn"
|
||||
? hasAnnounce
|
||||
? "announce"
|
||||
: hasDeliver
|
||||
? "deliver"
|
||||
: hasNoDeliver
|
||||
? "none"
|
||||
: "announce"
|
||||
: hasNoDeliver
|
||||
? "none"
|
||||
: "announce"
|
||||
: undefined;
|
||||
|
||||
const nameRaw = typeof opts.name === "string" ? opts.name : "";
|
||||
@@ -284,11 +233,9 @@ export function registerCronAddCommand(cron: Command) {
|
||||
? opts.channel.trim()
|
||||
: undefined,
|
||||
to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined,
|
||||
bestEffort:
|
||||
deliveryMode === "deliver" && opts.bestEffortDeliver ? true : undefined,
|
||||
bestEffort: opts.bestEffortDeliver ? true : undefined,
|
||||
}
|
||||
: undefined,
|
||||
isolation,
|
||||
};
|
||||
|
||||
const res = await callGatewayFromCli("cron.add", opts, params);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||
import {
|
||||
getCronChannelOptions,
|
||||
parseAtMs,
|
||||
parseAt,
|
||||
parseDurationMs,
|
||||
warnIfCronSchedulerDisabled,
|
||||
} from "./shared.js";
|
||||
@@ -47,11 +47,8 @@ export function registerCronEditCommand(cron: Command) {
|
||||
.option("--model <model>", "Model override for agent jobs")
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||
.option("--announce", "Announce summary to a chat (subagent-style)")
|
||||
.option(
|
||||
"--deliver",
|
||||
"Deliver full output to a chat (required when using last-route delivery without --to)",
|
||||
)
|
||||
.option("--no-deliver", "Disable delivery")
|
||||
.option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.")
|
||||
.option("--no-deliver", "Disable announce delivery")
|
||||
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`)
|
||||
.option(
|
||||
"--to <dest>",
|
||||
@@ -59,7 +56,6 @@ export function registerCronEditCommand(cron: Command) {
|
||||
)
|
||||
.option("--best-effort-deliver", "Do not fail job if delivery fails")
|
||||
.option("--no-best-effort-deliver", "Fail job when delivery fails")
|
||||
.option("--post-prefix <prefix>", "Prefix for summary system event")
|
||||
.action(async (id, opts) => {
|
||||
try {
|
||||
if (opts.session === "main" && opts.message) {
|
||||
@@ -72,11 +68,8 @@ export function registerCronEditCommand(cron: Command) {
|
||||
"Isolated jobs cannot use --system-event; use --message or --session main.",
|
||||
);
|
||||
}
|
||||
if (opts.session === "main" && typeof opts.postPrefix === "string") {
|
||||
throw new Error("--post-prefix only applies to isolated jobs.");
|
||||
}
|
||||
if (opts.announce && typeof opts.deliver === "boolean") {
|
||||
throw new Error("Choose --announce, --deliver, or --no-deliver (not multiple).");
|
||||
throw new Error("Choose --announce or --no-deliver (not multiple).");
|
||||
}
|
||||
|
||||
const patch: Record<string, unknown> = {};
|
||||
@@ -125,11 +118,11 @@ export function registerCronEditCommand(cron: Command) {
|
||||
throw new Error("Choose at most one schedule change");
|
||||
}
|
||||
if (opts.at) {
|
||||
const atMs = parseAtMs(String(opts.at));
|
||||
if (!atMs) {
|
||||
const atIso = parseAt(String(opts.at));
|
||||
if (!atIso) {
|
||||
throw new Error("Invalid --at");
|
||||
}
|
||||
patch.schedule = { kind: "at", atMs };
|
||||
patch.schedule = { kind: "at", at: atIso };
|
||||
} else if (opts.every) {
|
||||
const everyMs = parseDurationMs(String(opts.every));
|
||||
if (!everyMs) {
|
||||
@@ -164,7 +157,8 @@ export function registerCronEditCommand(cron: Command) {
|
||||
Boolean(thinking) ||
|
||||
hasTimeoutSeconds ||
|
||||
hasDeliveryModeFlag ||
|
||||
(!hasDeliveryModeFlag && (hasDeliveryTarget || hasBestEffort));
|
||||
hasDeliveryTarget ||
|
||||
hasBestEffort;
|
||||
if (hasSystemEventPatch && hasAgentTurnPatch) {
|
||||
throw new Error("Choose at most one payload change");
|
||||
}
|
||||
@@ -179,36 +173,16 @@ export function registerCronEditCommand(cron: Command) {
|
||||
assignIf(payload, "model", model, Boolean(model));
|
||||
assignIf(payload, "thinking", thinking, Boolean(thinking));
|
||||
assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds);
|
||||
if (!hasDeliveryModeFlag) {
|
||||
const channel =
|
||||
typeof opts.channel === "string" && opts.channel.trim()
|
||||
? opts.channel.trim()
|
||||
: undefined;
|
||||
const to = typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined;
|
||||
assignIf(payload, "channel", channel, Boolean(channel));
|
||||
assignIf(payload, "to", to, Boolean(to));
|
||||
assignIf(
|
||||
payload,
|
||||
"bestEffortDeliver",
|
||||
opts.bestEffortDeliver,
|
||||
typeof opts.bestEffortDeliver === "boolean",
|
||||
);
|
||||
}
|
||||
patch.payload = payload;
|
||||
}
|
||||
|
||||
if (typeof opts.postPrefix === "string") {
|
||||
patch.isolation = {
|
||||
postToMainPrefix: opts.postPrefix.trim() ? opts.postPrefix : "Cron",
|
||||
};
|
||||
}
|
||||
|
||||
if (hasDeliveryModeFlag) {
|
||||
const deliveryMode = opts.announce
|
||||
? "announce"
|
||||
: opts.deliver === true
|
||||
? "deliver"
|
||||
: "none";
|
||||
if (hasDeliveryModeFlag || hasDeliveryTarget || hasBestEffort) {
|
||||
const deliveryMode =
|
||||
opts.announce || opts.deliver === true
|
||||
? "announce"
|
||||
: opts.deliver === false
|
||||
? "none"
|
||||
: "announce";
|
||||
patch.delivery = {
|
||||
mode: deliveryMode,
|
||||
channel:
|
||||
|
||||
@@ -60,18 +60,18 @@ export function parseDurationMs(input: string): number | null {
|
||||
return Math.floor(n * factor);
|
||||
}
|
||||
|
||||
export function parseAtMs(input: string): number | null {
|
||||
export function parseAt(input: string): string | null {
|
||||
const raw = input.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const absolute = parseAbsoluteTimeMs(raw);
|
||||
if (absolute) {
|
||||
return absolute;
|
||||
return new Date(absolute).toISOString();
|
||||
}
|
||||
const dur = parseDurationMs(raw);
|
||||
if (dur) {
|
||||
return Date.now() + dur;
|
||||
return new Date(Date.now() + dur).toISOString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -97,13 +97,14 @@ const truncate = (value: string, width: number) => {
|
||||
return `${value.slice(0, width - 3)}...`;
|
||||
};
|
||||
|
||||
const formatIsoMinute = (ms: number) => {
|
||||
const d = new Date(ms);
|
||||
const formatIsoMinute = (iso: string) => {
|
||||
const parsed = parseAbsoluteTimeMs(iso);
|
||||
const d = new Date(parsed ?? NaN);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return "-";
|
||||
}
|
||||
const iso = d.toISOString();
|
||||
return `${iso.slice(0, 10)} ${iso.slice(11, 16)}Z`;
|
||||
const isoStr = d.toISOString();
|
||||
return `${isoStr.slice(0, 10)} ${isoStr.slice(11, 16)}Z`;
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
@@ -143,7 +144,7 @@ const formatRelative = (ms: number | null | undefined, nowMs: number) => {
|
||||
|
||||
const formatSchedule = (schedule: CronSchedule) => {
|
||||
if (schedule.kind === "at") {
|
||||
return `at ${formatIsoMinute(schedule.atMs)}`;
|
||||
return `at ${formatIsoMinute(schedule.at)}`;
|
||||
}
|
||||
if (schedule.kind === "every") {
|
||||
return `every ${formatDuration(schedule.everyMs)}`;
|
||||
|
||||
Reference in New Issue
Block a user