Skip to content

Commit de952c0

Browse files
committed
refactor: split cron delivery planning from sending
1 parent bd8d29c commit de952c0

6 files changed

Lines changed: 252 additions & 246 deletions

File tree

src/cron/delivery-plan.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import type { CronFailureDestinationConfig } from "../config/types.cron.js";
2+
import type { CronDelivery, CronDeliveryMode, CronJob, CronMessageChannel } from "./types.js";
3+
4+
export type CronDeliveryPlan = {
5+
mode: CronDeliveryMode;
6+
channel?: CronMessageChannel;
7+
to?: string;
8+
threadId?: string | number;
9+
/** Explicit channel account id from the delivery config, if set. */
10+
accountId?: string;
11+
source: "delivery";
12+
requested: boolean;
13+
};
14+
15+
function normalizeChannel(value: unknown): CronMessageChannel | undefined {
16+
if (typeof value !== "string") {
17+
return undefined;
18+
}
19+
const trimmed = value.trim().toLowerCase();
20+
if (!trimmed) {
21+
return undefined;
22+
}
23+
return trimmed as CronMessageChannel;
24+
}
25+
26+
function normalizeTo(value: unknown): string | undefined {
27+
if (typeof value !== "string") {
28+
return undefined;
29+
}
30+
const trimmed = value.trim();
31+
return trimmed ? trimmed : undefined;
32+
}
33+
34+
function normalizeAccountId(value: unknown): string | undefined {
35+
if (typeof value !== "string") {
36+
return undefined;
37+
}
38+
const trimmed = value.trim();
39+
return trimmed ? trimmed : undefined;
40+
}
41+
42+
function normalizeThreadId(value: unknown): string | number | undefined {
43+
if (typeof value === "number" && Number.isFinite(value)) {
44+
return value;
45+
}
46+
if (typeof value !== "string") {
47+
return undefined;
48+
}
49+
const trimmed = value.trim();
50+
return trimmed ? trimmed : undefined;
51+
}
52+
53+
export function resolveCronDeliveryPlan(job: CronJob): CronDeliveryPlan {
54+
const delivery = job.delivery;
55+
const hasDelivery = delivery && typeof delivery === "object";
56+
const rawMode = hasDelivery ? (delivery as { mode?: unknown }).mode : undefined;
57+
const normalizedMode = typeof rawMode === "string" ? rawMode.trim().toLowerCase() : rawMode;
58+
const mode =
59+
normalizedMode === "announce"
60+
? "announce"
61+
: normalizedMode === "webhook"
62+
? "webhook"
63+
: normalizedMode === "none"
64+
? "none"
65+
: normalizedMode === "deliver"
66+
? "announce"
67+
: undefined;
68+
69+
const deliveryChannel = normalizeChannel(
70+
(delivery as { channel?: unknown } | undefined)?.channel,
71+
);
72+
const deliveryTo = normalizeTo((delivery as { to?: unknown } | undefined)?.to);
73+
const deliveryThreadId = normalizeThreadId(
74+
(delivery as { threadId?: unknown } | undefined)?.threadId,
75+
);
76+
const channel = deliveryChannel ?? "last";
77+
const to = deliveryTo;
78+
const deliveryAccountId = normalizeAccountId(
79+
(delivery as { accountId?: unknown } | undefined)?.accountId,
80+
);
81+
if (hasDelivery) {
82+
const resolvedMode = mode ?? "announce";
83+
return {
84+
mode: resolvedMode,
85+
channel: resolvedMode === "announce" ? channel : undefined,
86+
to,
87+
threadId: resolvedMode === "announce" ? deliveryThreadId : undefined,
88+
accountId: deliveryAccountId,
89+
source: "delivery",
90+
requested: resolvedMode === "announce",
91+
};
92+
}
93+
94+
const isIsolatedAgentTurn =
95+
job.payload.kind === "agentTurn" &&
96+
(job.sessionTarget === "isolated" ||
97+
job.sessionTarget === "current" ||
98+
job.sessionTarget.startsWith("session:"));
99+
const resolvedMode = isIsolatedAgentTurn ? "announce" : "none";
100+
101+
return {
102+
mode: resolvedMode,
103+
channel: resolvedMode === "announce" ? "last" : undefined,
104+
to: undefined,
105+
threadId: undefined,
106+
source: "delivery",
107+
requested: resolvedMode === "announce",
108+
};
109+
}
110+
111+
export type CronFailureDeliveryPlan = {
112+
mode: "announce" | "webhook";
113+
channel?: CronMessageChannel;
114+
to?: string;
115+
accountId?: string;
116+
};
117+
118+
export type CronFailureDestinationInput = {
119+
channel?: CronMessageChannel;
120+
to?: string;
121+
accountId?: string;
122+
mode?: "announce" | "webhook";
123+
};
124+
125+
function normalizeFailureMode(value: unknown): "announce" | "webhook" | undefined {
126+
if (typeof value !== "string") {
127+
return undefined;
128+
}
129+
const trimmed = value.trim().toLowerCase();
130+
if (trimmed === "announce" || trimmed === "webhook") {
131+
return trimmed;
132+
}
133+
return undefined;
134+
}
135+
136+
export function resolveFailureDestination(
137+
job: CronJob,
138+
globalConfig?: CronFailureDestinationConfig,
139+
): CronFailureDeliveryPlan | null {
140+
const delivery = job.delivery;
141+
const jobFailureDest = delivery?.failureDestination as CronFailureDestinationInput | undefined;
142+
const hasJobFailureDest = jobFailureDest && typeof jobFailureDest === "object";
143+
144+
let channel: CronMessageChannel | undefined;
145+
let to: string | undefined;
146+
let accountId: string | undefined;
147+
let mode: "announce" | "webhook" | undefined;
148+
149+
if (globalConfig) {
150+
channel = normalizeChannel(globalConfig.channel);
151+
to = normalizeTo(globalConfig.to);
152+
accountId = normalizeAccountId(globalConfig.accountId);
153+
mode = normalizeFailureMode(globalConfig.mode);
154+
}
155+
156+
if (hasJobFailureDest) {
157+
const jobChannel = normalizeChannel(jobFailureDest.channel);
158+
const jobTo = normalizeTo(jobFailureDest.to);
159+
const jobAccountId = normalizeAccountId(jobFailureDest.accountId);
160+
const jobMode = normalizeFailureMode(jobFailureDest.mode);
161+
const hasJobChannelField = "channel" in jobFailureDest;
162+
const hasJobToField = "to" in jobFailureDest;
163+
const hasJobAccountIdField = "accountId" in jobFailureDest;
164+
165+
const jobToExplicitValue = hasJobToField && jobTo !== undefined;
166+
167+
if (hasJobChannelField) {
168+
channel = jobChannel;
169+
}
170+
if (hasJobToField) {
171+
to = jobTo;
172+
}
173+
if (hasJobAccountIdField) {
174+
accountId = jobAccountId;
175+
}
176+
if (jobMode !== undefined) {
177+
const globalMode = globalConfig?.mode ?? "announce";
178+
if (!jobToExplicitValue && globalMode !== jobMode) {
179+
to = undefined;
180+
}
181+
mode = jobMode;
182+
}
183+
}
184+
185+
if (!channel && !to && !accountId && !mode) {
186+
return null;
187+
}
188+
189+
const resolvedMode = mode ?? "announce";
190+
if (resolvedMode === "webhook" && !to) {
191+
return null;
192+
}
193+
194+
const result: CronFailureDeliveryPlan = {
195+
mode: resolvedMode,
196+
channel: resolvedMode === "announce" ? (channel ?? "last") : undefined,
197+
to,
198+
accountId,
199+
};
200+
201+
if (delivery && isSameDeliveryTarget(delivery, result)) {
202+
return null;
203+
}
204+
205+
return result;
206+
}
207+
208+
function isSameDeliveryTarget(
209+
delivery: CronDelivery,
210+
failurePlan: CronFailureDeliveryPlan,
211+
): boolean {
212+
const primaryMode = delivery.mode ?? "announce";
213+
if (primaryMode === "none") {
214+
return false;
215+
}
216+
217+
const primaryChannel = delivery.channel;
218+
const primaryTo = delivery.to;
219+
const primaryAccountId = delivery.accountId;
220+
221+
if (failurePlan.mode === "webhook") {
222+
return primaryMode === "webhook" && primaryTo === failurePlan.to;
223+
}
224+
225+
const primaryChannelNormalized = primaryChannel ?? "last";
226+
const failureChannelNormalized = failurePlan.channel ?? "last";
227+
228+
return (
229+
failureChannelNormalized === primaryChannelNormalized &&
230+
failurePlan.to === primaryTo &&
231+
failurePlan.accountId === primaryAccountId
232+
);
233+
}

src/cron/delivery.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { resolveCronDeliveryPlan, resolveFailureDestination } from "./delivery.js";
2+
import { resolveCronDeliveryPlan, resolveFailureDestination } from "./delivery-plan.js";
33
import type { CronJob } from "./types.js";
44

55
function makeJob(overrides: Partial<CronJob>): CronJob {

0 commit comments

Comments
 (0)