| |
| import { |
| MAX_DATE_TIMESTAMP_MS, |
| parseStrictNonNegativeInteger, |
| parseStrictPositiveInteger, |
| resolveExpiresAtMsFromDurationMs, |
| timestampMsToIsoString, |
| } from "@openclaw/normalization-core/number-coercion"; |
| import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; |
| import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; |
| import { GatewayClientRequestError } from "../../../packages/gateway-client/src/request-error.js"; |
| import { readCronJobNotFoundError } from "../../../packages/gateway-protocol/src/index.js"; |
| import { truncateToVisibleWidth, visibleWidth } from "../../../packages/terminal-core/src/ansi.js"; |
| import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js"; |
| import { colorize, isRich, theme } from "../../../packages/terminal-core/src/theme.js"; |
| import { listChannelPlugins } from "../../channels/plugins/index.js"; |
| import { parseAbsoluteTimeMs } from "../../cron/parse.js"; |
| import { resolveCronStaggerMs } from "../../cron/stagger.js"; |
| import type { CronDeliveryPreview, CronJob, CronSchedule } from "../../cron/types.js"; |
| import { danger } from "../../globals.js"; |
| import { formatErrorMessage } from "../../infra/errors.js"; |
| import { formatExactDuration } from "../../infra/format-time/format-duration-exact.js"; |
| import { formatDurationHuman } from "../../infra/format-time/format-duration.ts"; |
| import { parseOffsetlessIsoDateTimeInTimeZone } from "../../infra/format-time/parse-offsetless-zoned-datetime.js"; |
| import { formatTimestamp } from "../../logging/timestamps.js"; |
| import { defaultRuntime, ExitError, type RuntimeEnv } from "../../runtime.js"; |
| import { isOffsetlessIsoDateTime } from "../../shared/iso-time.js"; |
| import { formatLookupMiss } from "../error-format.js"; |
| import { |
| ExpectedCliError, |
| formatCliOperatorError, |
| rethrowExpectedCliError, |
| } from "../failure-output.js"; |
| import type { GatewayRpcOpts } from "../gateway-rpc.js"; |
| import { callGatewayFromCli } from "../gateway-rpc.js"; |
| import { isJsonOutputModeActive } from "../json-output-mode.js"; |
| import { exitCliAfterOutput } from "../one-shot-exit.js"; |
| import { parseDurationMs as parseSharedDurationMs } from "../parse-duration.js"; |
| import { CronCliError } from "./cron-cli-error.js"; |
|
|
| export function parseCronIntegerOption( |
| value: unknown, |
| flag: string, |
| kind: "positive" | "non-negative" = "positive", |
| ): number | undefined { |
| const parsed = |
| kind === "non-negative" |
| ? parseStrictNonNegativeInteger(value) |
| : parseStrictPositiveInteger(value); |
| if (value !== undefined && parsed === undefined) { |
| throw new CronCliError(`Invalid ${flag} (must be a ${kind} integer).`); |
| } |
| return parsed; |
| } |
|
|
| export function parseCronNoOutputTimeoutOption(opts: Record<string, unknown>): number | undefined { |
| |
| const raw = |
| opts.noOutputTimeoutSeconds ?? |
| (typeof opts.outputTimeoutSeconds === "string" || typeof opts.outputTimeoutSeconds === "number" |
| ? opts.outputTimeoutSeconds |
| : undefined); |
| return parseCronIntegerOption(raw, "--no-output-timeout-seconds"); |
| } |
|
|
| function parseCronArgv(value: unknown, flag: string): string[] | undefined { |
| if (typeof value !== "string") { |
| return undefined; |
| } |
| let parsed: unknown; |
| try { |
| parsed = JSON.parse(value); |
| } catch { |
| throw new CronCliError(`${flag} must be a JSON array of strings`); |
| } |
| if ( |
| !Array.isArray(parsed) || |
| parsed.length === 0 || |
| parsed.some((entry) => typeof entry !== "string" || entry.length === 0) |
| ) { |
| throw new CronCliError(`${flag} must be a non-empty JSON array of non-empty strings`); |
| } |
| return parsed; |
| } |
|
|
| export function parseCronCommandArgv(value: unknown): string[] | undefined { |
| return parseCronArgv(value, "--command-argv"); |
| } |
|
|
| export function parseCronStreamCommandArgv(value: unknown): string[] | undefined { |
| return parseCronArgv(value, "--stream-command"); |
| } |
|
|
| export function parseCronCommandEnv(values: unknown): Record<string, string> | undefined { |
| const rawValues = Array.isArray(values) ? values : typeof values === "string" ? [values] : []; |
| if (rawValues.length === 0) { |
| return undefined; |
| } |
| const env: Record<string, string> = {}; |
| for (const raw of rawValues) { |
| if (typeof raw !== "string") { |
| throw new CronCliError("--command-env must be KEY=VALUE"); |
| } |
| const idx = raw.indexOf("="); |
| const key = idx > 0 ? raw.slice(0, idx).trim() : ""; |
| if (!key) { |
| throw new CronCliError("--command-env must be KEY=VALUE"); |
| } |
| env[key] = raw.slice(idx + 1); |
| } |
| return env; |
| } |
|
|
| export const getCronChannelOptions = () => { |
| |
| |
| const pluginIds = listChannelPlugins() |
| .map((plugin) => plugin.id) |
| .filter(Boolean); |
| return pluginIds.length > 0 ? ["last", ...pluginIds].join("|") : "last|<channel-plugin-id>"; |
| }; |
|
|
| function toLocalIsoTime(value: unknown): string | undefined { |
| return typeof value === "number" && Number.isFinite(value) |
| ? formatTimestamp(new Date(value), { style: "long" }) |
| : undefined; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function enrichCronRunEntriesForDisplay(value: unknown): unknown { |
| if (!value || typeof value !== "object") { |
| return value; |
| } |
| const record = value as Record<string, unknown>; |
| const entries = record.entries; |
| if (!Array.isArray(entries)) { |
| return value; |
| } |
| const nextEntries = entries.map((entry) => { |
| if (!entry || typeof entry !== "object") { |
| return entry; |
| } |
| const item = entry as Record<string, unknown>; |
| if (item.action !== "finished") { |
| return item; |
| } |
| const extra: Record<string, unknown> = {}; |
| const cause = typeof item.errorReason === "string" ? item.errorReason.trim() : ""; |
| if (cause) { |
| extra.cause = cause; |
| } |
| const tsIso = toLocalIsoTime(item.ts); |
| if (tsIso) { |
| extra.tsIso = tsIso; |
| } |
| const runAtIso = toLocalIsoTime(item.runAtMs); |
| if (runAtIso) { |
| extra.runAtIso = runAtIso; |
| } |
| const nextRunAtIso = toLocalIsoTime(item.nextRunAtMs); |
| if (nextRunAtIso) { |
| extra.nextRunAtIso = nextRunAtIso; |
| } |
| return Object.keys(extra).length > 0 ? Object.assign({}, item, extra) : item; |
| }); |
| return { ...record, entries: nextEntries }; |
| } |
|
|
| export function printCronJson(value: unknown) { |
| defaultRuntime.writeJson(enrichCronRunEntriesForDisplay(value)); |
| } |
|
|
| |
| |
| |
| |
| |
| export function enrichCronJsonWithStatus(value: unknown): unknown { |
| if (!value || typeof value !== "object") { |
| return value; |
| } |
| const obj = value as Record<string, unknown>; |
|
|
| |
| if ("state" in obj && "enabled" in obj) { |
| return { ...obj, status: computeStatus(obj) }; |
| } |
|
|
| |
| if ("jobs" in obj && Array.isArray(obj.jobs)) { |
| const enrichedJobs = (obj.jobs as CronJob[]).map((job) => { |
| const status = computeStatus(job); |
| return Object.assign({}, job, { status }); |
| }); |
| return { ...obj, jobs: enrichedJobs }; |
| } |
|
|
| return value; |
| } |
|
|
| function computeStatus(job: { enabled?: unknown; state?: unknown }): string { |
| const state = asOptionalRecord(job.state) ?? {}; |
| if (state.runningAtMs) { |
| return "running"; |
| } |
| if (!job.enabled) { |
| return "disabled"; |
| } |
| return typeof state.lastRunStatus === "string" |
| ? state.lastRunStatus |
| : typeof state.lastStatus === "string" |
| ? state.lastStatus |
| : "idle"; |
| } |
|
|
| |
| |
| function decorateStatusWithFailures(status: string, consecutiveErrors: number | undefined): string { |
| const failures = consecutiveErrors ?? 0; |
| if (status !== "error" || failures <= 1) { |
| return status; |
| } |
| |
| |
| return failures > 99 ? `${status} (99+x)` : `${status} (${failures}x)`; |
| } |
|
|
| function formatCronStatusForDisplay(job: CronJob) { |
| const state = job.state ?? {}; |
| const status = computeStatus(job); |
| const streamDisabled = |
| job.enabled && job.schedule?.kind === "stream" && state.streamStatus === "disabled"; |
| const undelivered = status === "ok" && state.lastDeliveryStatus === "not-delivered"; |
| const suppressed = |
| undelivered && !streamDisabled && state.deliverySuppressionReason !== undefined; |
| |
| const color = |
| status === "error" |
| ? theme.error |
| : status === "running" || (undelivered && !suppressed) |
| ? theme.warn |
| : status === "ok" |
| ? theme.success |
| : theme.muted; |
| let label = decorateStatusWithFailures(status, state.consecutiveErrors); |
| if (streamDisabled && status !== "running") { |
| label = "disabled"; |
| } else if (status === "disabled" && state.autoDisabled) { |
| label = |
| state.autoDisabled.reason === "schedule-errors" |
| ? "disabled (schedule)" |
| : `disabled (${state.autoDisabled.consecutiveErrors}x)`; |
| } else if (undelivered) { |
| label = suppressed ? "ok (suppressed)" : "ok (not delivered)"; |
| } |
| return { label, color }; |
| } |
|
|
| export function handleCronCliError(err: unknown) { |
| |
| if (err instanceof ExitError) { |
| throw err; |
| } |
| rethrowExpectedCliError(err); |
| const missingJob = readCronJobNotFoundError(err); |
| const diagnostic = err instanceof CronCliError ? (err.originalError ?? err) : err; |
| if (isJsonOutputModeActive(process.argv)) { |
| if ( |
| !missingJob && |
| !(err instanceof CronCliError) && |
| !(err instanceof GatewayClientRequestError) |
| ) { |
| throw err; |
| } |
| |
| |
| const message = missingJob |
| ? formatCronLookupMiss(missingJob.jobId) |
| : formatCliOperatorError(diagnostic); |
| throw new ExpectedCliError({ |
| message, |
| humanOutput: danger(message), |
| machineOutput: message, |
| }); |
| } |
| const message = missingJob |
| ? formatCronLookupMiss(missingJob.jobId) |
| : formatErrorMessage(diagnostic); |
| defaultRuntime.error(danger(message)); |
| exitCliAfterOutput(defaultRuntime, 1); |
| } |
|
|
| export const formatCronLookupMiss = (jobId: string) => |
| formatLookupMiss({ |
| noun: "Automation", |
| value: sanitizeTerminalText(jobId), |
| listCommand: "openclaw cron list", |
| valueLabel: "automation id", |
| }); |
|
|
| |
| |
| export function requireCronJobId(id: unknown, accepted = "Pass it positionally."): string { |
| const jobId = normalizeOptionalString(id); |
| if (!jobId) { |
| throw new CronCliError(`Missing job id. ${accepted}`); |
| } |
| return jobId; |
| } |
|
|
| export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) { |
| |
| try { |
| const res = (await callGatewayFromCli("cron.status", opts, {})) as { |
| enabled?: boolean; |
| storePath?: string; |
| storage?: string; |
| sqlitePath?: string; |
| }; |
| if (res?.enabled !== false) { |
| return; |
| } |
| const store = |
| typeof res?.sqlitePath === "string" |
| ? res.sqlitePath |
| : typeof res?.storePath === "string" |
| ? res.storePath |
| : ""; |
| defaultRuntime.error( |
| [ |
| "warning: the automations scheduler is disabled in the Gateway; jobs are saved but will not run automatically.", |
| "To enable automatic runs, set `cron.enabled: true` (or remove `cron.enabled: false`), remove `OPENCLAW_SKIP_CRON=1` from the Gateway's launch environment, and restart the Gateway.", |
| store ? `store: ${store}` : "", |
| ] |
| .filter(Boolean) |
| .join("\n"), |
| ); |
| } catch { |
| |
| } |
| } |
|
|
| export function parsePositiveCronDurationMs(input: string): number | null { |
| try { |
| const result = parseSharedDurationMs(input); |
| return result > 0 && result <= MAX_DATE_TIMESTAMP_MS ? result : null; |
| } catch { |
| return null; |
| } |
| } |
|
|
| export function parseCronStaggerMs(params: { |
| staggerRaw: string; |
| useExact: boolean; |
| }): number | undefined { |
| if (params.useExact) { |
| return 0; |
| } |
| if (!params.staggerRaw) { |
| return undefined; |
| } |
| const parsed = parsePositiveCronDurationMs(params.staggerRaw); |
| if (!parsed) { |
| throw new CronCliError("Invalid --stagger; use e.g. 30s, 1m, 5m"); |
| } |
| return parsed; |
| } |
|
|
| export function parseCronStringList(input: unknown): string[] | undefined { |
| if (input === undefined) { |
| return undefined; |
| } |
| const raw = Array.isArray(input) |
| ? input.map((value) => String(value)).join(" ") |
| : typeof input === "string" |
| ? input |
| : ""; |
| return raw |
| .split(/[,\s]+/u) |
| .map((entry) => normalizeOptionalString(entry)) |
| .filter((entry): entry is string => Boolean(entry)); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function parseAt(input: string, tz?: string): string | null { |
| const raw = input.trim(); |
| if (!raw) { |
| return null; |
| } |
|
|
| |
| |
| if (tz && isOffsetlessIsoDateTime(raw)) { |
| return parseOffsetlessIsoDateTimeInTimeZone(raw, tz); |
| } |
|
|
| const absolute = parseAbsoluteTimeMs(raw); |
| if (absolute !== null) { |
| return timestampMsToIsoString(absolute) ?? null; |
| } |
| const durationInput = raw.startsWith("+") ? raw.slice(1) : raw; |
| const dur = parsePositiveCronDurationMs(durationInput); |
| if (dur !== null) { |
| const expiresAt = resolveExpiresAtMsFromDurationMs(dur); |
| return timestampMsToIsoString(expiresAt) ?? null; |
| } |
| return null; |
| } |
|
|
| const CRON_ID_PAD = 36; |
| const CRON_DECLARATION_PAD = 24; |
| const CRON_NAME_PAD = 24; |
| const CRON_SCHEDULE_PAD = 32; |
| const CRON_NEXT_PAD = 10; |
| const CRON_LAST_PAD = 10; |
| const CRON_STATUS_PAD = 19; |
| const CRON_TARGET_PAD = 9; |
| const CRON_DELIVERY_PAD = 64; |
| const CRON_AGENT_PAD = 10; |
| const CRON_OWNER_PAD = 24; |
| const CRON_MODEL_PAD = 20; |
| const TRUNCATED_SUFFIX = "..."; |
|
|
| const stringifyCell = (value: unknown, fallback = "-") => { |
| if (typeof value === "string") { |
| return value; |
| } |
| if (typeof value === "number" || typeof value === "boolean") { |
| return String(value); |
| } |
| return fallback; |
| }; |
|
|
| const formatCell = (value: unknown, width: number) => { |
| const text = sanitizeTerminalText(stringifyCell(value)); |
| const truncated = |
| visibleWidth(text) <= width |
| ? text |
| : width <= TRUNCATED_SUFFIX.length |
| ? truncateToVisibleWidth(text, width) |
| : `${truncateToVisibleWidth(text, width - TRUNCATED_SUFFIX.length)}${TRUNCATED_SUFFIX}`; |
| const remaining = width - visibleWidth(truncated); |
| return remaining > 0 ? `${truncated}${" ".repeat(remaining)}` : truncated; |
| }; |
|
|
| const formatIsoMinute = (iso: string) => { |
| const isoStr = timestampMsToIsoString(parseAbsoluteTimeMs(iso)); |
| |
| return isoStr ? `${isoStr.slice(0, -8).replace("T", " ")}Z` : "-"; |
| }; |
|
|
| const formatSpan = (ms: number) => (ms < 60_000 ? "<1m" : formatDurationHuman(ms)); |
|
|
| const formatRelative = (ms: number | null | undefined, nowMs: number) => { |
| if (!ms) { |
| return "-"; |
| } |
| const delta = ms - nowMs; |
| const label = formatSpan(Math.abs(delta)); |
| return delta >= 0 ? `in ${label}` : `${label} ago`; |
| }; |
|
|
| const formatSchedule = (schedule: CronSchedule | undefined, hasTrigger = false) => { |
| const suffix = hasTrigger ? "+trigger" : ""; |
| if (schedule?.kind === "at") { |
| return `at ${formatIsoMinute(schedule.at)}${suffix}`; |
| } |
| if (schedule?.kind === "every") { |
| return `every ${formatExactDuration(schedule.everyMs)}${suffix}`; |
| } |
| if (schedule?.kind === "on-exit") { |
| const cwd = schedule.cwd ? ` @ ${schedule.cwd}` : ""; |
| return `on-exit ${schedule.command}${cwd}`; |
| } |
| if (schedule?.kind === "stream") { |
| const cwd = schedule.cwd ? ` @ ${schedule.cwd}` : ""; |
| return `stream ${schedule.command.join(" ")}${cwd}${suffix}`; |
| } |
| if (schedule?.kind !== "cron") { |
| return "-"; |
| } |
| const base = schedule.tz |
| ? `cron ${schedule.expr} @ ${schedule.tz}${suffix}` |
| : `cron ${schedule.expr}${suffix}`; |
| const staggerMs = resolveCronStaggerMs(schedule); |
| if (staggerMs <= 0) { |
| return `${base} (exact)`; |
| } |
| return `${base} (stagger ${formatExactDuration(staggerMs)})`; |
| }; |
|
|
| export function coerceCronDeliveryPreviews(value: unknown): Map<string, CronDeliveryPreview> { |
| const previews = |
| value && typeof value === "object" |
| ? (value as { deliveryPreviews?: unknown }).deliveryPreviews |
| : undefined; |
| if (!previews || typeof previews !== "object") { |
| return new Map(); |
| } |
| return new Map( |
| Object.entries(previews as Record<string, unknown>).flatMap(([jobId, preview]) => { |
| if (!preview || typeof preview !== "object") { |
| return []; |
| } |
| const record = preview as { label?: unknown; detail?: unknown }; |
| if (typeof record.label !== "string" || typeof record.detail !== "string") { |
| return []; |
| } |
| return [[jobId, { label: record.label, detail: record.detail }]]; |
| }), |
| ); |
| } |
|
|
| export function printCronList( |
| jobs: Array<CronJob & { effectiveAgentId?: string | null }>, |
| runtime: RuntimeEnv = defaultRuntime, |
| opts?: { deliveryPreviews?: Map<string, CronDeliveryPreview> }, |
| ) { |
| if (jobs.length === 0) { |
| runtime.log("No automations."); |
| return; |
| } |
|
|
| const rich = isRich(); |
| const header = [ |
| formatCell("ID", CRON_ID_PAD), |
| formatCell("Declaration", CRON_DECLARATION_PAD), |
| formatCell("Name", CRON_NAME_PAD), |
| formatCell("Schedule", CRON_SCHEDULE_PAD), |
| formatCell("Next", CRON_NEXT_PAD), |
| formatCell("Last", CRON_LAST_PAD), |
| formatCell("Status", CRON_STATUS_PAD), |
| formatCell("Target", CRON_TARGET_PAD), |
| formatCell("Delivery", CRON_DELIVERY_PAD), |
| formatCell("Agent ID", CRON_AGENT_PAD), |
| formatCell("Owner", CRON_OWNER_PAD), |
| formatCell("Model", CRON_MODEL_PAD), |
| ].join(" "); |
|
|
| const lines = [rich ? theme.heading(header) : header]; |
| const now = Date.now(); |
|
|
| for (const job of jobs) { |
| const state = job.state ?? {}; |
| const idLabel = formatCell(job.id, CRON_ID_PAD); |
| const declarationLabel = formatCell(job.declarationKey, CRON_DECLARATION_PAD); |
| const nameLabel = formatCell(job.displayName ?? job.name, CRON_NAME_PAD); |
| const scheduleLabel = formatCell( |
| formatSchedule(job.schedule, job.trigger !== undefined), |
| CRON_SCHEDULE_PAD, |
| ); |
| const nextLabel = formatCell( |
| job.enabled ? formatRelative(state.nextRunAtMs, now) : "-", |
| CRON_NEXT_PAD, |
| ); |
| const lastLabel = formatCell(formatRelative(state.lastRunAtMs, now), CRON_LAST_PAD); |
| const status = formatCronStatusForDisplay(job); |
| const statusLabel = formatCell(status.label, CRON_STATUS_PAD); |
| const targetLabel = formatCell(job.sessionTarget, CRON_TARGET_PAD); |
| const deliveryPreview = opts?.deliveryPreviews?.get(job.id); |
| const deliveryText = deliveryPreview |
| ? `${deliveryPreview.label} (${deliveryPreview.detail})` |
| : "-"; |
| const deliveryLabel = formatCell(deliveryText, CRON_DELIVERY_PAD); |
| const agentId = job.effectiveAgentId ?? job.agentId; |
| const agentLabel = formatCell(agentId ?? "unresolved", CRON_AGENT_PAD); |
| const ownerLabel = formatCell(job.owner?.sessionKey ?? job.owner?.agentId, CRON_OWNER_PAD); |
| const modelLabel = formatCell( |
| job.payload?.kind === "agentTurn" ? job.payload.model : undefined, |
| CRON_MODEL_PAD, |
| ); |
|
|
| const coloredTarget = |
| job.sessionTarget === "main" |
| ? colorize(rich, theme.accent, targetLabel) |
| : colorize(rich, theme.accentBright, targetLabel); |
| const coloredAgent = agentId |
| ? colorize(rich, theme.info, agentLabel) |
| : colorize(rich, theme.muted, agentLabel); |
|
|
| const line = [ |
| colorize(rich, theme.accent, idLabel), |
| colorize(rich, theme.muted, declarationLabel), |
| colorize(rich, theme.info, nameLabel), |
| colorize(rich, theme.info, scheduleLabel), |
| colorize(rich, theme.muted, nextLabel), |
| colorize(rich, theme.muted, lastLabel), |
| colorize(rich, status.color, statusLabel), |
| coloredTarget, |
| deliveryPreview |
| ? colorize(rich, theme.info, deliveryLabel) |
| : colorize(rich, theme.muted, deliveryLabel), |
| coloredAgent, |
| colorize(rich, job.owner ? theme.info : theme.muted, ownerLabel), |
| job.payload?.kind === "agentTurn" && job.payload.model |
| ? colorize(rich, theme.info, modelLabel) |
| : colorize(rich, theme.muted, modelLabel), |
| ].join(" "); |
|
|
| lines.push(line.trimEnd()); |
| } |
|
|
| runtime.log(lines.join("\n")); |
| } |
|
|
| export function printCronShow( |
| job: CronJob, |
| runtime: RuntimeEnv = defaultRuntime, |
| opts?: { deliveryPreview?: CronDeliveryPreview }, |
| ) { |
| const preview = opts?.deliveryPreview ?? { label: "-", detail: "unavailable" }; |
| const showValue = (value: unknown) => sanitizeTerminalText(stringifyCell(value)); |
| runtime.log(`id: ${showValue(job.id)}`); |
| runtime.log(`declaration: ${showValue(job.declarationKey)}`); |
| runtime.log(`name: ${showValue(job.name)}`); |
| runtime.log(`display name: ${showValue(job.displayName)}`); |
| runtime.log(`owner agent: ${showValue(job.owner?.agentId)}`); |
| runtime.log(`owner session: ${showValue(job.owner?.sessionKey)}`); |
| runtime.log(`enabled: ${job.enabled ? "yes" : "no"}`); |
| runtime.log(`schedule: ${showValue(formatSchedule(job.schedule, job.trigger !== undefined))}`); |
| if (job.schedule?.kind === "stream") { |
| runtime.log(`stream status: ${showValue(job.state.streamStatus)}`); |
| runtime.log(`stream error: ${showValue(job.state.streamError)}`); |
| } |
| runtime.log( |
| `trigger: ${job.trigger ? `once=${job.trigger.once === true ? "yes" : "no"}; evals=${job.state.triggerEvalCount ?? 0}; last eval=${formatRelative(job.state.lastTriggerEvalAtMs, Date.now())}; last fire=${formatRelative(job.state.lastTriggerFireAtMs, Date.now())}` : "-"}`, |
| ); |
| runtime.log(`session: ${showValue(job.sessionTarget)}`); |
| runtime.log(`agent: ${showValue(job.agentId)}`); |
| runtime.log( |
| `model: ${showValue(job.payload.kind === "agentTurn" ? job.payload.model : undefined)}`, |
| ); |
| runtime.log(`delivery: ${showValue(preview.label)} (${showValue(preview.detail)})`); |
| runtime.log(`next: ${formatRelative(job.state.nextRunAtMs, Date.now())}`); |
| runtime.log(`last: ${formatRelative(job.state.lastRunAtMs, Date.now())}`); |
| runtime.log(`status: ${showValue(formatCronStatusForDisplay(job).label)}`); |
| |
| |
| runtime.log(`last error: ${showValue(job.state.lastError)}`); |
| runtime.log(`last delivery: ${showValue(job.state.lastDeliveryStatus)}`); |
| runtime.log(`last delivery suppression: ${showValue(job.state.deliverySuppressionReason)}`); |
| runtime.log(`last delivery error: ${showValue(job.state.lastDeliveryError)}`); |
| runtime.log(`diagnostic: ${showValue(job.state.lastDiagnosticSummary)}`); |
| } |
|
|