feat(cron): introduce delivery modes for isolated jobs

- Added support for new delivery modes in cron jobs: `announce`, `deliver`, and `none`.
- Updated documentation to reflect changes in delivery options and usage examples.
- Enhanced the cron job schema to include delivery configuration.
- Refactored related CLI commands and UI components to accommodate the new delivery settings.
- Improved handling of legacy delivery fields for backward compatibility.

This update allows users to choose how output from isolated jobs is delivered, enhancing flexibility in job management.
This commit is contained in:
Tyler Yust
2026-02-03 13:44:29 -08:00
committed by Peter Steinberger
parent 3a03e38378
commit 511c656cbc
24 changed files with 604 additions and 205 deletions

View File

@@ -1,5 +1,7 @@
import crypto from "node:crypto";
import type {
CronDelivery,
CronDeliveryPatch,
CronJob,
CronJobCreate,
CronJobPatch,
@@ -26,6 +28,12 @@ export function assertSupportedJobSpec(job: Pick<CronJob, "sessionTarget" | "pay
}
}
function assertDeliverySupport(job: Pick<CronJob, "sessionTarget" | "delivery">) {
if (job.delivery && job.sessionTarget !== "isolated") {
throw new Error('cron delivery config is only supported for sessionTarget="isolated"');
}
}
export function findJobOrThrow(state: CronServiceState, id: string) {
const job = state.store?.jobs.find((j) => j.id === id);
if (!job) {
@@ -102,12 +110,14 @@ export function createJob(state: CronServiceState, input: CronJobCreate): CronJo
sessionTarget: input.sessionTarget,
wakeMode: input.wakeMode,
payload: input.payload,
delivery: input.delivery,
isolation: input.isolation,
state: {
...input.state,
},
};
assertSupportedJobSpec(job);
assertDeliverySupport(job);
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
return job;
}
@@ -137,6 +147,9 @@ export function applyJobPatch(job: CronJob, patch: CronJobPatch) {
if (patch.payload) {
job.payload = mergeCronPayload(job.payload, patch.payload);
}
if (patch.delivery) {
job.delivery = mergeCronDelivery(job.delivery, patch.delivery);
}
if (patch.isolation) {
job.isolation = patch.isolation;
}
@@ -147,6 +160,7 @@ export function applyJobPatch(job: CronJob, patch: CronJobPatch) {
job.agentId = normalizeOptionalAgentId((patch as { agentId?: unknown }).agentId);
}
assertSupportedJobSpec(job);
assertDeliverySupport(job);
}
function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronPayload {
@@ -219,6 +233,35 @@ function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload {
};
}
function mergeCronDelivery(
existing: CronDelivery | undefined,
patch: CronDeliveryPatch,
): CronDelivery {
const next: CronDelivery = {
mode: existing?.mode ?? "none",
channel: existing?.channel,
to: existing?.to,
bestEffort: existing?.bestEffort,
};
if (typeof patch.mode === "string") {
next.mode = patch.mode;
}
if ("channel" in patch) {
const channel = typeof patch.channel === "string" ? patch.channel.trim() : "";
next.channel = channel ? channel : undefined;
}
if ("to" in patch) {
const to = typeof patch.to === "string" ? patch.to.trim() : "";
next.to = to ? to : undefined;
}
if (typeof patch.bestEffort === "boolean") {
next.bestEffort = patch.bestEffort;
}
return next;
}
export function isJobDue(job: CronJob, nowMs: number, opts: { forced: boolean }) {
if (opts.forced) {
return true;

View File

@@ -125,7 +125,7 @@ export async function executeJob(
emit(state, { jobId: job.id, action: "removed" });
}
if (job.sessionTarget === "isolated") {
if (job.sessionTarget === "isolated" && !job.delivery) {
const prefix = job.isolation?.postToMainPrefix?.trim() || "Cron";
const mode = job.isolation?.postToMainMode ?? "summary";