refactor: consolidate cron delivery boundary parsing

This commit is contained in:
Peter Steinberger 2026-04-01 16:22:06 +09:00
parent 340c99d657
commit 86b519850e
No known key found for this signature in database
3 changed files with 163 additions and 156 deletions

View File

@ -0,0 +1,79 @@
import { z, type ZodType } from "zod";
const trimStringPreprocess = (value: unknown) => (typeof value === "string" ? value.trim() : value);
const trimLowercaseStringPreprocess = (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : value;
export const DeliveryModeFieldSchema = z
.preprocess(trimLowercaseStringPreprocess, z.enum(["deliver", "announce", "none", "webhook"]))
.transform((value) => (value === "deliver" ? "announce" : value));
export const LowercaseNonEmptyStringFieldSchema = z.preprocess(
trimLowercaseStringPreprocess,
z.string().min(1),
);
export const TrimmedNonEmptyStringFieldSchema = z.preprocess(
trimStringPreprocess,
z.string().min(1),
);
export const DeliveryThreadIdFieldSchema = z.union([
TrimmedNonEmptyStringFieldSchema,
z.number().finite(),
]);
export const LegacyDeliveryThreadIdFieldSchema = DeliveryThreadIdFieldSchema.transform((value) =>
String(value),
);
export const TimeoutSecondsFieldSchema = z
.number()
.finite()
.transform((value) => Math.max(0, Math.floor(value)));
export type ParsedDeliveryInput = {
mode?: "announce" | "none" | "webhook";
channel?: string;
to?: string;
threadId?: string | number;
accountId?: string;
};
export function parseDeliveryInput(input: Record<string, unknown>): ParsedDeliveryInput {
return {
mode: parseOptionalField(DeliveryModeFieldSchema, input.mode),
channel: parseOptionalField(LowercaseNonEmptyStringFieldSchema, input.channel),
to: parseOptionalField(TrimmedNonEmptyStringFieldSchema, input.to),
threadId: parseOptionalField(DeliveryThreadIdFieldSchema, input.threadId),
accountId: parseOptionalField(TrimmedNonEmptyStringFieldSchema, input.accountId),
};
}
export type ParsedLegacyDeliveryHints = {
deliver?: boolean;
bestEffortDeliver?: boolean;
channel?: string;
provider?: string;
to?: string;
threadId?: string;
};
export function parseLegacyDeliveryHintsInput(
payload: Record<string, unknown>,
): ParsedLegacyDeliveryHints {
return {
deliver: parseOptionalField(z.boolean(), payload.deliver),
bestEffortDeliver: parseOptionalField(z.boolean(), payload.bestEffortDeliver),
channel: parseOptionalField(LowercaseNonEmptyStringFieldSchema, payload.channel),
provider: parseOptionalField(LowercaseNonEmptyStringFieldSchema, payload.provider),
to: parseOptionalField(TrimmedNonEmptyStringFieldSchema, payload.to),
threadId: parseOptionalField(LegacyDeliveryThreadIdFieldSchema, payload.threadId),
};
}
export function parseOptionalField<T>(schema: ZodType<T>, value: unknown): T | undefined {
const parsed = schema.safeParse(value);
return parsed.success ? parsed.data : undefined;
}

View File

@ -1,107 +1,71 @@
import { parseLegacyDeliveryHintsInput } from "./delivery-field-schemas.js";
export function hasLegacyDeliveryHints(payload: Record<string, unknown>) {
if (typeof payload.deliver === "boolean") {
return true;
}
if (typeof payload.bestEffortDeliver === "boolean") {
return true;
}
if (typeof payload.channel === "string" && payload.channel.trim()) {
return true;
}
if (typeof payload.provider === "string" && payload.provider.trim()) {
return true;
}
if (typeof payload.to === "string" && payload.to.trim()) {
return true;
}
if (typeof payload.threadId === "string" && payload.threadId.trim()) {
return true;
}
if (typeof payload.threadId === "number" && Number.isFinite(payload.threadId)) {
return true;
}
return false;
const hints = parseLegacyDeliveryHintsInput(payload);
return (
hints.deliver !== undefined ||
hints.bestEffortDeliver !== undefined ||
hints.channel !== undefined ||
hints.provider !== undefined ||
hints.to !== undefined ||
hints.threadId !== undefined
);
}
export function buildDeliveryFromLegacyPayload(
payload: Record<string, unknown>,
): Record<string, unknown> {
const deliver = payload.deliver;
const mode = deliver === false ? "none" : "announce";
const channelRaw =
typeof payload.channel === "string" && payload.channel.trim()
? payload.channel.trim().toLowerCase()
: typeof payload.provider === "string"
? payload.provider.trim().toLowerCase()
: "";
const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
const threadIdRaw =
typeof payload.threadId === "string"
? payload.threadId.trim()
: typeof payload.threadId === "number" && Number.isFinite(payload.threadId)
? String(payload.threadId)
: "";
const hints = parseLegacyDeliveryHintsInput(payload);
const mode = hints.deliver === false ? "none" : "announce";
const next: Record<string, unknown> = { mode };
if (channelRaw) {
next.channel = channelRaw;
if (hints.channel ?? hints.provider) {
next.channel = hints.channel ?? hints.provider;
}
if (toRaw) {
next.to = toRaw;
if (hints.to) {
next.to = hints.to;
}
if (threadIdRaw) {
next.threadId = threadIdRaw;
if (hints.threadId) {
next.threadId = hints.threadId;
}
if (typeof payload.bestEffortDeliver === "boolean") {
next.bestEffort = payload.bestEffortDeliver;
if (hints.bestEffortDeliver !== undefined) {
next.bestEffort = hints.bestEffortDeliver;
}
return next;
}
export function buildDeliveryPatchFromLegacyPayload(payload: Record<string, unknown>) {
const deliver = payload.deliver;
const channelRaw =
typeof payload.channel === "string" && payload.channel.trim()
? payload.channel.trim().toLowerCase()
: typeof payload.provider === "string" && payload.provider.trim()
? payload.provider.trim().toLowerCase()
: "";
const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
const threadIdRaw =
typeof payload.threadId === "string"
? payload.threadId.trim()
: typeof payload.threadId === "number" && Number.isFinite(payload.threadId)
? String(payload.threadId)
: "";
const hints = parseLegacyDeliveryHintsInput(payload);
const next: Record<string, unknown> = {};
let hasPatch = false;
if (deliver === false) {
if (hints.deliver === false) {
next.mode = "none";
hasPatch = true;
} else if (
deliver === true ||
channelRaw ||
toRaw ||
threadIdRaw ||
typeof payload.bestEffortDeliver === "boolean"
hints.deliver === true ||
hints.channel ||
hints.provider ||
hints.to ||
hints.threadId ||
hints.bestEffortDeliver !== undefined
) {
next.mode = "announce";
hasPatch = true;
}
if (channelRaw) {
next.channel = channelRaw;
if (hints.channel ?? hints.provider) {
next.channel = hints.channel ?? hints.provider;
hasPatch = true;
}
if (toRaw) {
next.to = toRaw;
if (hints.to) {
next.to = hints.to;
hasPatch = true;
}
if (threadIdRaw) {
next.threadId = threadIdRaw;
if (hints.threadId) {
next.threadId = hints.threadId;
hasPatch = true;
}
if (typeof payload.bestEffortDeliver === "boolean") {
next.bestEffort = payload.bestEffortDeliver;
if (hints.bestEffortDeliver !== undefined) {
next.bestEffort = hints.bestEffortDeliver;
hasPatch = true;
}

View File

@ -1,5 +1,13 @@
import { sanitizeAgentId } from "../routing/session-key.js";
import { isRecord } from "../utils.js";
import {
DeliveryThreadIdFieldSchema,
TimeoutSecondsFieldSchema,
TrimmedNonEmptyStringFieldSchema,
parseDeliveryInput,
parseLegacyDeliveryHintsInput,
parseOptionalField,
} from "./delivery-field-schemas.js";
import { normalizeLegacyDeliveryInput } from "./legacy-delivery.js";
import { parseAbsoluteTimeMs } from "./parse.js";
import { migrateLegacyCronPayload } from "./payload-migration.js";
@ -124,32 +132,25 @@ function coercePayload(payload: UnknownRecord) {
}
}
if ("model" in next) {
if (typeof next.model === "string") {
const trimmed = next.model.trim();
if (trimmed) {
next.model = trimmed;
} else {
delete next.model;
}
const model = parseOptionalField(TrimmedNonEmptyStringFieldSchema, next.model);
if (model !== undefined) {
next.model = model;
} else {
delete next.model;
}
}
if ("thinking" in next) {
if (typeof next.thinking === "string") {
const trimmed = next.thinking.trim();
if (trimmed) {
next.thinking = trimmed;
} else {
delete next.thinking;
}
const thinking = parseOptionalField(TrimmedNonEmptyStringFieldSchema, next.thinking);
if (thinking !== undefined) {
next.thinking = thinking;
} else {
delete next.thinking;
}
}
if ("timeoutSeconds" in next) {
if (typeof next.timeoutSeconds === "number" && Number.isFinite(next.timeoutSeconds)) {
next.timeoutSeconds = Math.max(0, Math.floor(next.timeoutSeconds));
const timeoutSeconds = parseOptionalField(TimeoutSecondsFieldSchema, next.timeoutSeconds);
if (timeoutSeconds !== undefined) {
next.timeoutSeconds = timeoutSeconds;
} else {
delete next.timeoutSeconds;
}
@ -180,54 +181,30 @@ function coercePayload(payload: UnknownRecord) {
function coerceDelivery(delivery: UnknownRecord) {
const next: UnknownRecord = { ...delivery };
if (typeof delivery.mode === "string") {
const mode = delivery.mode.trim().toLowerCase();
if (mode === "deliver") {
next.mode = "announce";
} else if (mode === "announce" || mode === "none" || mode === "webhook") {
next.mode = mode;
} else {
delete next.mode;
}
const parsed = parseDeliveryInput(delivery);
if (parsed.mode !== undefined) {
next.mode = parsed.mode;
} else if ("mode" in next) {
delete next.mode;
}
if (typeof delivery.channel === "string") {
const trimmed = delivery.channel.trim().toLowerCase();
if (trimmed) {
next.channel = trimmed;
} else {
delete next.channel;
}
if (parsed.channel !== undefined) {
next.channel = parsed.channel;
} else if ("channel" in next) {
delete next.channel;
}
if (typeof delivery.to === "string") {
const trimmed = delivery.to.trim();
if (trimmed) {
next.to = trimmed;
} else {
delete next.to;
}
if (parsed.to !== undefined) {
next.to = parsed.to;
} else if ("to" in next) {
delete next.to;
}
if (typeof delivery.threadId === "number" && Number.isFinite(delivery.threadId)) {
next.threadId = delivery.threadId;
} else if (typeof delivery.threadId === "string") {
const trimmed = delivery.threadId.trim();
if (trimmed) {
next.threadId = trimmed;
} else {
delete next.threadId;
}
if (parsed.threadId !== undefined) {
next.threadId = parsed.threadId;
} else if ("threadId" in next) {
delete next.threadId;
}
if (typeof delivery.accountId === "string") {
const trimmed = delivery.accountId.trim();
if (trimmed) {
next.accountId = trimmed;
} else {
delete next.accountId;
}
} else if ("accountId" in next && typeof next.accountId !== "string") {
if (parsed.accountId !== undefined) {
next.accountId = parsed.accountId;
} else if ("accountId" in next) {
delete next.accountId;
}
return next;
@ -298,38 +275,25 @@ function copyTopLevelAgentTurnFields(next: UnknownRecord, payload: UnknownRecord
}
function copyTopLevelLegacyDeliveryFields(next: UnknownRecord, payload: UnknownRecord) {
if (typeof payload.deliver !== "boolean" && typeof next.deliver === "boolean") {
payload.deliver = next.deliver;
const hints = parseLegacyDeliveryHintsInput(next);
if (typeof payload.deliver !== "boolean" && hints.deliver !== undefined) {
payload.deliver = hints.deliver;
}
if (
typeof payload.channel !== "string" &&
typeof next.channel === "string" &&
next.channel.trim()
) {
payload.channel = next.channel.trim();
if (typeof payload.channel !== "string" && hints.channel !== undefined) {
payload.channel = hints.channel;
}
if (typeof payload.to !== "string" && typeof next.to === "string" && next.to.trim()) {
payload.to = next.to.trim();
if (typeof payload.to !== "string" && hints.to !== undefined) {
payload.to = hints.to;
}
if (
!("threadId" in payload) &&
((typeof next.threadId === "number" && Number.isFinite(next.threadId)) ||
(typeof next.threadId === "string" && next.threadId.trim()))
) {
payload.threadId = typeof next.threadId === "string" ? next.threadId.trim() : next.threadId;
const threadId = parseOptionalField(DeliveryThreadIdFieldSchema, next.threadId);
if (!("threadId" in payload) && threadId !== undefined) {
payload.threadId = threadId;
}
if (
typeof payload.bestEffortDeliver !== "boolean" &&
typeof next.bestEffortDeliver === "boolean"
) {
payload.bestEffortDeliver = next.bestEffortDeliver;
if (typeof payload.bestEffortDeliver !== "boolean" && hints.bestEffortDeliver !== undefined) {
payload.bestEffortDeliver = hints.bestEffortDeliver;
}
if (
typeof payload.provider !== "string" &&
typeof next.provider === "string" &&
next.provider.trim()
) {
payload.provider = next.provider.trim();
if (typeof payload.provider !== "string" && hints.provider !== undefined) {
payload.provider = hints.provider;
}
}