feat(cron): enhance legacy delivery handling in job patches

- Introduced logic to map legacy payload delivery updates onto the delivery object for `agentTurn` jobs, ensuring backward compatibility with legacy clients.
- Added tests to validate the correct application of legacy delivery settings in job patches, improving reliability in job configuration.
- Refactored delivery handling functions to streamline the merging of legacy delivery fields into the current job structure.

This update enhances the flexibility of delivery configurations, ensuring that legacy settings are properly handled in the context of new job patches.
This commit is contained in:
Tyler Yust
2026-02-03 20:15:43 -08:00
committed by Peter Steinberger
parent 246896d64b
commit 6fb8d8850e
3 changed files with 196 additions and 0 deletions

View File

@@ -29,4 +29,75 @@ describe("applyJobPatch", () => {
expect(job.payload.kind).toBe("systemEvent");
expect(job.delivery).toBeUndefined();
});
it("maps legacy payload delivery updates onto delivery", () => {
const now = Date.now();
const job: CronJob = {
id: "job-2",
name: "job-2",
enabled: true,
createdAtMs: now,
updatedAtMs: now,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "do it" },
delivery: { mode: "announce", channel: "telegram", to: "123" },
state: {},
};
const patch: CronJobPatch = {
payload: {
kind: "agentTurn",
deliver: false,
channel: "Signal",
to: "555",
bestEffortDeliver: true,
},
};
expect(() => applyJobPatch(job, patch)).not.toThrow();
expect(job.payload.kind).toBe("agentTurn");
if (job.payload.kind === "agentTurn") {
expect(job.payload.deliver).toBe(false);
expect(job.payload.channel).toBe("Signal");
expect(job.payload.to).toBe("555");
expect(job.payload.bestEffortDeliver).toBe(true);
}
expect(job.delivery).toEqual({
mode: "none",
channel: "signal",
to: "555",
bestEffort: true,
});
});
it("treats legacy payload targets as announce requests", () => {
const now = Date.now();
const job: CronJob = {
id: "job-3",
name: "job-3",
enabled: true,
createdAtMs: now,
updatedAtMs: now,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "do it" },
delivery: { mode: "none", channel: "telegram" },
state: {},
};
const patch: CronJobPatch = {
payload: { kind: "agentTurn", to: " 999 " },
};
expect(() => applyJobPatch(job, patch)).not.toThrow();
expect(job.delivery).toEqual({
mode: "announce",
channel: "telegram",
to: "999",
bestEffort: undefined,
});
});
});

View File

@@ -155,6 +155,17 @@ export function applyJobPatch(job: CronJob, patch: CronJobPatch) {
if (patch.payload) {
job.payload = mergeCronPayload(job.payload, patch.payload);
}
if (!patch.delivery && patch.payload?.kind === "agentTurn") {
// Back-compat: legacy clients still update delivery via payload fields.
const legacyDeliveryPatch = buildLegacyDeliveryPatch(patch.payload);
if (
legacyDeliveryPatch &&
job.sessionTarget === "isolated" &&
job.payload.kind === "agentTurn"
) {
job.delivery = mergeCronDelivery(job.delivery, legacyDeliveryPatch);
}
}
if (patch.delivery) {
job.delivery = mergeCronDelivery(job.delivery, patch.delivery);
}
@@ -216,6 +227,47 @@ function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronP
return next;
}
function buildLegacyDeliveryPatch(
payload: Extract<CronPayloadPatch, { kind: "agentTurn" }>,
): CronDeliveryPatch | null {
const deliver = payload.deliver;
const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
const hasLegacyHints =
typeof deliver === "boolean" ||
typeof payload.bestEffortDeliver === "boolean" ||
Boolean(toRaw);
if (!hasLegacyHints) {
return null;
}
const patch: CronDeliveryPatch = {};
let hasPatch = false;
if (deliver === false) {
patch.mode = "none";
hasPatch = true;
} else if (deliver === true || toRaw) {
patch.mode = "announce";
hasPatch = true;
}
if (typeof payload.channel === "string") {
const channel = payload.channel.trim().toLowerCase();
patch.channel = channel ? channel : undefined;
hasPatch = true;
}
if (typeof payload.to === "string") {
patch.to = payload.to.trim();
hasPatch = true;
}
if (typeof payload.bestEffortDeliver === "boolean") {
patch.bestEffort = payload.bestEffortDeliver;
hasPatch = true;
}
return hasPatch ? patch : null;
}
function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload {
if (patch.kind === "systemEvent") {
if (typeof patch.text !== "string" || patch.text.length === 0) {

View File

@@ -39,6 +39,69 @@ function buildDeliveryFromLegacyPayload(payload: Record<string, unknown>) {
return next;
}
function buildDeliveryPatchFromLegacyPayload(payload: Record<string, unknown>) {
const deliver = payload.deliver;
const channelRaw =
typeof payload.channel === "string" ? payload.channel.trim().toLowerCase() : "";
const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
const next: Record<string, unknown> = {};
let hasPatch = false;
if (deliver === false) {
next.mode = "none";
hasPatch = true;
} else if (deliver === true || toRaw) {
next.mode = "announce";
hasPatch = true;
}
if (channelRaw) {
next.channel = channelRaw;
hasPatch = true;
}
if (toRaw) {
next.to = toRaw;
hasPatch = true;
}
if (typeof payload.bestEffortDeliver === "boolean") {
next.bestEffort = payload.bestEffortDeliver;
hasPatch = true;
}
return hasPatch ? next : null;
}
function mergeLegacyDeliveryInto(
delivery: Record<string, unknown>,
payload: Record<string, unknown>,
) {
const patch = buildDeliveryPatchFromLegacyPayload(payload);
if (!patch) {
return { delivery, mutated: false };
}
const next = { ...delivery };
let mutated = false;
if ("mode" in patch && patch.mode !== next.mode) {
next.mode = patch.mode;
mutated = true;
}
if ("channel" in patch && patch.channel !== next.channel) {
next.channel = patch.channel;
mutated = true;
}
if ("to" in patch && patch.to !== next.to) {
next.to = patch.to;
mutated = true;
}
if ("bestEffort" in patch && patch.bestEffort !== next.bestEffort) {
next.bestEffort = patch.bestEffort;
mutated = true;
}
return { delivery: next, mutated };
}
function stripLegacyDeliveryFields(payload: Record<string, unknown>) {
if ("deliver" in payload) {
delete payload.deliver;
@@ -180,6 +243,16 @@ export async function ensureLoaded(state: CronServiceState) {
mutated = true;
}
if (payloadRecord && hasLegacyDelivery) {
if (hasDelivery) {
const merged = mergeLegacyDeliveryInto(
delivery as Record<string, unknown>,
payloadRecord,
);
if (merged.mutated) {
raw.delivery = merged.delivery;
mutated = true;
}
}
stripLegacyDeliveryFields(payloadRecord);
mutated = true;
}