UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,521 lines 62.4 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, l as normalizeOptionalStringifiedId, p as readStringValue, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js";
import { P as timestampMsToIsoString } from "./number-coercion-CJQ8TR--.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { g as shortenHomePath } from "./utils-CCC-BEJH.js";
import { t as parseJsonWithJson5Fallback } from "./parse-json-compat-DvZKmwhP.js";
import { i as resolveAgentModelPrimaryValue } from "./model-input-B2zxl6MM.js";
import { At as boolean } from "./schemas-6cH6bZ7o.js";
import { V as MEMORY_DREAMING_SYSTEM_EVENT_TEXT } from "./dreaming-SqhFI2Et.js";
import { a as LowercaseNonEmptyStringFieldSchema, i as DeliveryThreadIdFieldSchema, n as normalizeCronJobInput, o as TrimmedNonEmptyStringFieldSchema, s as parseOptionalField } from "./normalize-Da7MTCo5.js";
import { i as loadCronQuarantineFile, l as saveCronJobsStore, o as resolveCronJobsStorePath, p as getInvalidPersistedCronJobReason, r as loadCronJobsStoreWithConfigJobs, s as resolveCronQuarantinePath, u as saveCronQuarantineFile } from "./store-_jAmB352.js";
import { i as parseAbsoluteTimeMs, r as resolveDefaultCronStaggerMs, t as normalizeCronStaggerMs } from "./stagger-Bbv08nIY.js";
import { d as coerceFiniteScheduleNumber, s as inferCronJobName } from "./session-target-DNkvuLUI.js";
import { r as readCronRunLogEntriesPage, s as parseCronRunLogEntryObject, t as appendCronRunLog } from "./run-log-qm3LS7cj.js";
import { t as normalizeHttpWebhookUrl } from "./webhook-url-DDwLAmTp.js";
import { t as note } from "./note-BRSJp0UF.js";
import "./schedule-VhNUS91b.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import { isDeepStrictEqual, promisify } from "node:util";
//#region src/commands/doctor/cron/dreaming-payload-migration.ts
function isManagedDreamingJob(raw) {
	if (normalizeOptionalString(raw.description)?.includes("[managed-by=memory-core.short-term-promotion]")) return true;
	if (normalizeOptionalString(raw.name) !== "Memory Dreaming Promotion") return false;
	const payload = raw.payload ?? void 0;
	const payloadKind = normalizeOptionalLowercaseString(payload?.kind);
	if (payloadKind === "systemevent") return normalizeOptionalString(payload?.text) === MEMORY_DREAMING_SYSTEM_EVENT_TEXT;
	if (payloadKind === "agentturn") return normalizeOptionalString(payload?.message) === MEMORY_DREAMING_SYSTEM_EVENT_TEXT;
	return false;
}
function isStaleDreamingJob(raw) {
	if (normalizeOptionalLowercaseString(raw.sessionTarget) !== "isolated") return true;
	const payload = raw.payload ?? void 0;
	if (normalizeOptionalLowercaseString(payload?.kind) !== "agentturn") return true;
	if (payload?.lightContext !== true) return true;
	if (normalizeOptionalLowercaseString((raw.delivery ?? void 0)?.mode) !== "none") return true;
	return false;
}
function rewriteDreamingJobShape(raw) {
	raw.sessionTarget = "isolated";
	raw.payload = {
		kind: "agentTurn",
		message: MEMORY_DREAMING_SYSTEM_EVENT_TEXT,
		lightContext: true
	};
	raw.delivery = { mode: "none" };
}
/** Rewrite managed dreaming jobs to the isolated light-context agent-turn shape. */
function migrateLegacyDreamingPayloadShape(jobs) {
	let rewrittenCount = 0;
	for (const raw of jobs) {
		if (!isManagedDreamingJob(raw)) continue;
		if (!isStaleDreamingJob(raw)) continue;
		rewriteDreamingJobShape(raw);
		rewrittenCount += 1;
	}
	return {
		changed: rewrittenCount > 0,
		rewrittenCount
	};
}
/** Count managed dreaming jobs that still need payload/session/delivery migration. */
function countStaleDreamingJobs(jobs) {
	let count = 0;
	for (const raw of jobs) if (isManagedDreamingJob(raw) && isStaleDreamingJob(raw)) count += 1;
	return count;
}
//#endregion
//#region src/commands/doctor/cron/legacy-notify.ts
/** Migrate legacy notify fallback flags into explicit delivery destinations when possible. */
function migrateLegacyNotifyFallback(params) {
	let changed = false;
	const warnings = [];
	const configuredLegacyWebhook = normalizeOptionalString(params.legacyWebhook);
	const legacyWebhook = configuredLegacyWebhook ? normalizeHttpWebhookUrl(configuredLegacyWebhook) : void 0;
	const warnMissingLegacyWebhook = (jobName) => {
		warnings.push(configuredLegacyWebhook ? `Cron job "${jobName}" still uses legacy notify fallback, but cron.webhook is not a valid HTTP(S) URL so doctor cannot migrate it automatically.` : `Cron job "${jobName}" still uses legacy notify fallback, but cron.webhook is unset so doctor cannot migrate it automatically.`);
	};
	for (const raw of params.jobs) {
		if (!("notify" in raw)) continue;
		const jobName = normalizeOptionalString(raw.name) ?? normalizeOptionalString(raw.id) ?? "<unnamed>";
		if (!(raw.notify === true)) {
			delete raw.notify;
			changed = true;
			continue;
		}
		const delivery = raw.delivery && typeof raw.delivery === "object" && !Array.isArray(raw.delivery) ? raw.delivery : null;
		const mode = normalizeOptionalLowercaseString(delivery?.mode);
		const to = normalizeOptionalString(delivery?.to);
		const hasLegacyChatDelivery = mode === void 0 && delivery !== null && (normalizeOptionalString(delivery.channel) !== void 0 || normalizeOptionalString(delivery.accountId) !== void 0 || "threadId" in delivery || to !== void 0 && !normalizeHttpWebhookUrl(to));
		const completionDestination = delivery?.completionDestination && typeof delivery.completionDestination === "object" && !Array.isArray(delivery.completionDestination) ? delivery.completionDestination : null;
		const completionMode = normalizeOptionalLowercaseString(completionDestination?.mode);
		const completionTo = normalizeOptionalString(completionDestination?.to);
		const validWebhookTo = to ? normalizeHttpWebhookUrl(to) : void 0;
		const validCompletionTo = completionTo ? normalizeHttpWebhookUrl(completionTo) : void 0;
		if (mode === "webhook" && validWebhookTo || completionMode === "webhook" && validCompletionTo) {
			delete raw.notify;
			changed = true;
			continue;
		}
		if (mode === void 0 && !hasLegacyChatDelivery || mode === "none" || mode === "webhook") {
			if (!legacyWebhook) {
				warnMissingLegacyWebhook(jobName);
				continue;
			}
			raw.delivery = {
				...delivery,
				mode: "webhook",
				to: mode === "none" ? legacyWebhook : validWebhookTo ?? legacyWebhook
			};
			delete raw.notify;
			changed = true;
			continue;
		}
		if (legacyWebhook) {
			raw.delivery = {
				...delivery,
				...hasLegacyChatDelivery ? { mode: "announce" } : {},
				completionDestination: {
					...completionDestination,
					mode: "webhook",
					to: legacyWebhook
				}
			};
			delete raw.notify;
			changed = true;
			continue;
		}
		warnMissingLegacyWebhook(jobName);
	}
	return {
		changed,
		warnings
	};
}
//#endregion
//#region src/cron/run-log-jsonl.ts
/** Parses legacy cron run-log JSONL, skipping malformed or non-matching rows. */
function parseCronRunLogEntriesFromJsonl(raw, opts) {
	if (!raw.trim()) return [];
	const parsed = [];
	for (const line of raw.split("\n")) {
		const trimmed = line.trim();
		if (!trimmed) continue;
		try {
			const entry = parseCronRunLogEntryObject(JSON.parse(trimmed), opts);
			if (entry) parsed.push(entry);
		} catch {}
	}
	return parsed;
}
//#endregion
//#region src/commands/doctor/cron/legacy-run-log-migration.ts
const LEGACY_CRON_RUN_LOG_ARCHIVE_SUFFIX = ".migrated";
function legacyRunLogKey(entry) {
	return [
		entry.jobId,
		entry.ts,
		entry.runId ?? "",
		entry.status ?? "",
		entry.summary ?? "",
		entry.error ?? ""
	].join("\0");
}
async function readExistingRunLogKeys(params) {
	const keys = /* @__PURE__ */ new Set();
	let offset = 0;
	while (true) {
		const page = await readCronRunLogEntriesPage({
			storePath: params.storePath,
			jobId: params.jobId,
			limit: 200,
			offset,
			sortDir: "asc"
		});
		for (const entry of page.entries) keys.add(legacyRunLogKey(entry));
		if (!page.hasMore) return keys;
		offset = page.nextOffset ?? offset + page.entries.length;
	}
}
async function importLegacyCronRunLog(filePath, params) {
	const resolved = path.resolve(filePath);
	if (!fs.existsSync(resolved)) return;
	const existingKeys = await readExistingRunLogKeys(params);
	const raw = fs.readFileSync(resolved, "utf-8");
	for (const entry of parseCronRunLogEntriesFromJsonl(raw, { jobId: params.jobId })) {
		const key = legacyRunLogKey(entry);
		if (existingKeys.has(key)) continue;
		existingKeys.add(key);
		await appendCronRunLog({
			storePath: params.storePath,
			entry,
			opts: { keepLines: false }
		});
	}
	archiveLegacyCronRunLogSync(resolved);
}
function archiveLegacyCronRunLogSync(filePath) {
	const archivePath = `${filePath}${LEGACY_CRON_RUN_LOG_ARCHIVE_SUFFIX}`;
	if (!fs.existsSync(filePath) || fs.existsSync(archivePath)) return;
	try {
		fs.renameSync(filePath, archivePath);
	} catch {}
}
/** Import legacy per-job JSONL run logs into SQLite and archive migrated files. */
async function migrateLegacyCronRunLogsToSqlite(storePath) {
	const resolvedStorePath = path.resolve(storePath);
	const runsDir = path.resolve(path.dirname(resolvedStorePath), "runs");
	const jsonlFiles = (await fs$1.readdir(runsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"));
	for (const file of jsonlFiles) {
		const jobId = path.basename(file.name, ".jsonl");
		await importLegacyCronRunLog(path.join(runsDir, file.name), {
			storePath: resolvedStorePath,
			jobId
		});
	}
	return { importedFiles: jsonlFiles.length };
}
/** Return true when legacy cron JSONL run log files exist next to a store path. */
async function legacyCronRunLogFilesExist(storePath) {
	const resolvedStorePath = path.resolve(storePath);
	const runsDir = path.resolve(path.dirname(resolvedStorePath), "runs");
	return (await fs$1.readdir(runsDir, { withFileTypes: true }).catch(() => [])).some((entry) => entry.isFile() && entry.name.endsWith(".jsonl"));
}
//#endregion
//#region src/commands/doctor/cron/legacy-store-migration.ts
const LEGACY_CRON_ARCHIVE_SUFFIX = ".migrated";
function resolveLegacyCronStatePath(storePath) {
	if (storePath.endsWith(".json")) return storePath.replace(/\.json$/, "-state.json");
	return `${storePath}-state.json`;
}
async function legacyCronFileExists(filePath) {
	return fs$1.access(filePath).then(() => true).catch(() => false);
}
async function archiveLegacyCronFile(filePath) {
	if (!await legacyCronFileExists(filePath)) return;
	let archivePath = `${filePath}${LEGACY_CRON_ARCHIVE_SUFFIX}`;
	for (let index = 2; await legacyCronFileExists(archivePath); index += 1) archivePath = `${filePath}${LEGACY_CRON_ARCHIVE_SUFFIX}.${index}`;
	await fs$1.rename(filePath, archivePath).catch(() => void 0);
}
function parseCronStateFile(raw) {
	try {
		const parsed = parseJsonWithJson5Fallback(raw);
		if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
		const record = parsed;
		if (record.version !== 1 || typeof record.jobs !== "object" || record.jobs === null || Array.isArray(record.jobs)) return null;
		return {
			version: 1,
			jobs: record.jobs
		};
	} catch {
		return null;
	}
}
function readString(record, key) {
	return normalizeOptionalString(record[key]);
}
function readNumber(record, key) {
	return coerceFiniteScheduleNumber(record[key]);
}
function legacySchedulePayloadFromRecord(schedule) {
	const rawKind = readString(schedule, "kind")?.toLowerCase();
	const expr = readString(schedule, "expr") ?? readString(schedule, "cron");
	const at = readString(schedule, "at");
	const atMs = readNumber(schedule, "atMs");
	const everyMs = readNumber(schedule, "everyMs");
	const anchorMs = readNumber(schedule, "anchorMs");
	const tz = readString(schedule, "tz");
	const staggerMs = normalizeCronStaggerMs(schedule.staggerMs);
	const kind = rawKind === "at" || rawKind === "every" || rawKind === "cron" ? rawKind : at || atMs !== void 0 ? "at" : everyMs !== void 0 ? "every" : expr ? "cron" : void 0;
	if (kind === "at") return at ? {
		kind: "at",
		at
	} : atMs !== void 0 ? {
		kind: "at",
		at: String(atMs)
	} : void 0;
	if (kind === "every" && everyMs !== void 0) return {
		kind: "every",
		everyMs,
		anchorMs
	};
	if (kind === "cron" && expr) return {
		kind: "cron",
		expr,
		tz,
		staggerMs
	};
}
function tryLegacyCronScheduleIdentity(job) {
	const schedule = job.schedule && typeof job.schedule === "object" && !Array.isArray(job.schedule) ? legacySchedulePayloadFromRecord(job.schedule) : legacySchedulePayloadFromRecord(job);
	if (!schedule) return;
	return JSON.stringify({
		version: 1,
		enabled: typeof job.enabled === "boolean" ? job.enabled : true,
		schedule
	});
}
function getRawCronJobs(parsed) {
	return Array.isArray(parsed) ? parsed : isRecord(parsed) && Array.isArray(parsed.jobs) ? parsed.jobs : [];
}
function cloneConfigJobs(jobs) {
	return jobs.map((job) => structuredClone(job));
}
async function loadStateFile(statePath) {
	let raw;
	try {
		raw = await fs$1.readFile(statePath, "utf-8");
	} catch (err) {
		if (err?.code === "ENOENT") return null;
		throw new Error(`Failed to read cron state at ${statePath}: ${String(err)}`, { cause: err });
	}
	return parseCronStateFile(raw);
}
function hasInlineState(jobs) {
	return jobs.some((job) => job != null && isRecord(job.state) && Object.keys(job.state).length > 0);
}
function ensureJobStateObject(job) {
	if (!isRecord(job.state)) job.state = {};
}
function backfillMissingRuntimeFields(job) {
	ensureJobStateObject(job);
	if (typeof job.updatedAtMs !== "number") job.updatedAtMs = typeof job.createdAtMs === "number" ? job.createdAtMs : Date.now();
}
function resolveUpdatedAtMs(job, updatedAtMs) {
	if (typeof updatedAtMs === "number" && Number.isFinite(updatedAtMs)) return updatedAtMs;
	if (typeof job.updatedAtMs === "number" && Number.isFinite(job.updatedAtMs)) return job.updatedAtMs;
	return typeof job.createdAtMs === "number" && Number.isFinite(job.createdAtMs) ? job.createdAtMs : Date.now();
}
function mergeStateFileEntry(job, entry) {
	if (!isRecord(entry)) {
		backfillMissingRuntimeFields(job);
		return;
	}
	job.updatedAtMs = resolveUpdatedAtMs(job, entry.updatedAtMs);
	job.state = isRecord(entry.state) ? entry.state : {};
	if (typeof entry.scheduleIdentity === "string" && entry.scheduleIdentity !== tryLegacyCronScheduleIdentity(job)) {
		ensureJobStateObject(job);
		job.state.nextRunAtMs = void 0;
	}
}
function resolveCronStateId(job) {
	return normalizeOptionalString(job.id) ?? normalizeOptionalString(job.jobId);
}
/** Return true when legacy cron JSON or state files exist for a store path. */
async function legacyCronStoreFilesExist(storePath) {
	const resolvedStorePath = path.resolve(storePath);
	return await legacyCronFileExists(resolvedStorePath) || await legacyCronFileExists(resolveLegacyCronStatePath(resolvedStorePath));
}
/** Rename legacy cron JSON/state files after successful migration. */
async function archiveLegacyCronStoreForMigration(storePath) {
	const resolvedStorePath = path.resolve(storePath);
	await Promise.all([archiveLegacyCronFile(resolvedStorePath), archiveLegacyCronFile(resolveLegacyCronStatePath(resolvedStorePath))]);
}
/** Load legacy cron JSON/state files into the current loaded-store shape for migration. */
async function loadLegacyCronStoreForMigration(storePath) {
	const resolvedStorePath = path.resolve(storePath);
	try {
		const raw = await fs$1.readFile(resolvedStorePath, "utf-8");
		let parsed;
		try {
			parsed = parseJsonWithJson5Fallback(raw);
		} catch (err) {
			throw new Error(`Failed to parse cron store at ${resolvedStorePath}: ${String(err)}`, { cause: err });
		}
		const rawJobs = getRawCronJobs(parsed);
		const configJobIndexes = [];
		const configRows = [];
		const configJobRuntimeEntries = [];
		const invalidConfigRows = [];
		for (const [index, row] of rawJobs.entries()) if (isRecord(row)) {
			configJobIndexes.push(index);
			configRows.push(row);
		} else invalidConfigRows.push({
			sourceIndex: index,
			reason: "non-object-row",
			raw: structuredClone(row)
		});
		const store = {
			version: 1,
			jobs: configRows
		};
		const jobs = store.jobs;
		const configJobs = cloneConfigJobs(configRows);
		const stateFile = await loadStateFile(resolveLegacyCronStatePath(resolvedStorePath));
		const hasLegacyInlineState = !stateFile && hasInlineState(jobs);
		if (stateFile) for (const job of store.jobs) {
			const stateId = resolveCronStateId(job);
			const entry = stateId ? stateFile.jobs[stateId] : void 0;
			configJobRuntimeEntries.push(isRecord(entry) ? structuredClone(entry) : {});
			if (entry) mergeStateFileEntry(job, entry);
			else backfillMissingRuntimeFields(job);
		}
		else if (!hasLegacyInlineState) for (const job of store.jobs) backfillMissingRuntimeFields(job);
		for (const job of store.jobs) ensureJobStateObject(job);
		return {
			store,
			configJobs,
			configJobIndexes,
			configJobRuntimeEntries,
			invalidConfigRows
		};
	} catch (err) {
		if (err?.code === "ENOENT") return {
			store: {
				version: 1,
				jobs: []
			},
			configJobs: [],
			configJobIndexes: [],
			configJobRuntimeEntries: [],
			invalidConfigRows: []
		};
		throw err;
	}
}
//#endregion
//#region src/commands/doctor/cron/repair-plan.ts
function pluralize$2(count, noun) {
	return `${count} ${noun}${count === 1 ? "" : "s"}`;
}
function formatJobNameList(names) {
	if (!names || names.length === 0) return "";
	const preview = names.slice(0, 5).map((name) => `\`${name}\``);
	const remaining = names.length - preview.length;
	return remaining > 0 ? `: ${preview.join(", ")} (+${remaining} more)` : `: ${preview.join(", ")}`;
}
/** Convert legacy cron issue counts into doctor preview lines. */
function formatLegacyIssuePreview(issues, details = {}) {
	const lines = [];
	if (issues.jobId) lines.push(`- ${pluralize$2(issues.jobId, "job")} still uses legacy \`jobId\``);
	if (issues.missingId) lines.push(`- ${pluralize$2(issues.missingId, "job")} is missing a canonical string \`id\``);
	if (issues.nonStringId) lines.push(`- ${pluralize$2(issues.nonStringId, "job")} stores \`id\` as a non-string value`);
	if (issues.legacyScheduleString) lines.push(`- ${pluralize$2(issues.legacyScheduleString, "job")} stores schedule as a bare string`);
	if (issues.legacyScheduleCron) lines.push(`- ${pluralize$2(issues.legacyScheduleCron, "job")} still uses \`schedule.cron\``);
	if (issues.legacyPayloadKind) lines.push(`- ${pluralize$2(issues.legacyPayloadKind, "job")} needs payload kind normalization`);
	if (issues.legacyPayloadCodexModel) lines.push(`- ${pluralize$2(issues.legacyPayloadCodexModel, "job")} still uses legacy \`openai-codex/*\` cron model refs`);
	if (issues.legacyAgentTurnCommandPayload) lines.push(`- ${pluralize$2(issues.legacyAgentTurnCommandPayload, "job")} uses an agent prompt to run a shell command`);
	if (issues.unresolvedAgentTurnShellToolPrompt) lines.push(`- ${pluralize$2(issues.unresolvedAgentTurnShellToolPrompt, "job")} asks an isolated agent for shell/process tools and needs manual command conversion${formatJobNameList(details.unresolvedAgentTurnShellToolPrompt)}`);
	if (issues.legacyPayloadProvider) lines.push(`- ${pluralize$2(issues.legacyPayloadProvider, "job")} still uses payload \`provider\` as a delivery alias`);
	if (issues.legacyTopLevelPayloadFields) lines.push(`- ${pluralize$2(issues.legacyTopLevelPayloadFields, "job")} still uses top-level payload fields`);
	if (issues.legacyTopLevelDeliveryFields) lines.push(`- ${pluralize$2(issues.legacyTopLevelDeliveryFields, "job")} still uses top-level delivery fields`);
	if (issues.legacyDeliveryMode) lines.push(`- ${pluralize$2(issues.legacyDeliveryMode, "job")} still uses delivery mode \`deliver\``);
	if (issues.invalidSchedule) lines.push(`- ${pluralize$2(issues.invalidSchedule, "job")} has an invalid persisted schedule and will be removed`);
	if (issues.invalidPayload) lines.push(`- ${pluralize$2(issues.invalidPayload, "job")} has an invalid persisted payload and will be removed`);
	return lines;
}
function cronJobMigrationKey(job) {
	return normalizeOptionalString(job.id) ?? normalizeOptionalString(job.jobId);
}
/** Merge legacy JSON jobs into current jobs without duplicating matching ids/jobIds. */
function mergeLegacyCronJobs(params) {
	const merged = [...params.currentJobs];
	const currentKeys = new Set(params.currentJobs.map((job) => cronJobMigrationKey(job)).filter((key) => key !== void 0));
	let importedCount = 0;
	for (const legacyJob of params.legacyJobs) {
		const key = cronJobMigrationKey(legacyJob);
		if (key && currentKeys.has(key)) continue;
		if (key) currentKeys.add(key);
		merged.push(legacyJob);
		importedCount += 1;
	}
	return {
		jobs: merged,
		importedCount
	};
}
/** Attach runtime SQLite state columns back onto a config-defined cron job row. */
function mergeRuntimeEntryIntoConfigJob(params) {
	return {
		...params.job,
		...params.runtimeEntry?.updatedAtMs !== void 0 ? { updatedAtMs: params.runtimeEntry.updatedAtMs } : {},
		...params.runtimeEntry?.state ? { state: structuredClone(params.runtimeEntry.state) } : {}
	};
}
/** Return true when a SQLite cron projection row no longer matches config JSON. */
function needsSqliteProjectionBackfill(params) {
	if (!params.projectedJob) return true;
	const normalizedConfig = normalizeCronJobInput(params.configJob, { applyDefaults: true });
	if (!normalizedConfig) return true;
	const projected = params.projectedJob;
	for (const field of [
		"agentId",
		"deleteAfterRun",
		"delivery",
		"description",
		"enabled",
		"failureAlert",
		"name",
		"payload",
		"schedule",
		"sessionKey",
		"sessionTarget",
		"wakeMode"
	]) if (!isDeepStrictEqual(normalizedConfig[field], projected[field])) return true;
	return false;
}
//#endregion
//#region src/commands/doctor/cron/legacy-delivery.ts
function parseLegacyDeliveryHintsInput(payload) {
	return {
		deliver: parseOptionalField(boolean(), payload.deliver),
		bestEffortDeliver: parseOptionalField(boolean(), payload.bestEffortDeliver),
		channel: parseOptionalField(LowercaseNonEmptyStringFieldSchema, payload.channel),
		provider: parseOptionalField(LowercaseNonEmptyStringFieldSchema, payload.provider),
		to: parseOptionalField(TrimmedNonEmptyStringFieldSchema, payload.to),
		threadId: parseOptionalField(DeliveryThreadIdFieldSchema.transform((value) => String(value)), payload.threadId)
	};
}
/** Return true when a payload still carries legacy delivery hint fields. */
function hasLegacyDeliveryHints(payload) {
	const hints = parseLegacyDeliveryHintsInput(payload);
	return hints.deliver !== void 0 || hints.bestEffortDeliver !== void 0 || hints.channel !== void 0 || hints.provider !== void 0 || hints.to !== void 0 || hints.threadId !== void 0;
}
/** Build a new delivery object from legacy top-level payload delivery fields. */
function buildDeliveryFromLegacyPayload(payload) {
	const hints = parseLegacyDeliveryHintsInput(payload);
	const next = { mode: hints.deliver === false ? "none" : "announce" };
	if (hints.channel ?? hints.provider) next.channel = hints.channel ?? hints.provider;
	if (hints.to) next.to = hints.to;
	if (hints.threadId) next.threadId = hints.threadId;
	if (hints.bestEffortDeliver !== void 0) next.bestEffort = hints.bestEffortDeliver;
	return next;
}
/** Build a partial delivery patch from legacy payload fields, or null when none exist. */
function buildDeliveryPatchFromLegacyPayload(payload) {
	const hints = parseLegacyDeliveryHintsInput(payload);
	const next = {};
	let hasPatch = false;
	if (hints.deliver === false) {
		next.mode = "none";
		hasPatch = true;
	} else if (hints.deliver === true || hints.channel || hints.provider || hints.to || hints.threadId || hints.bestEffortDeliver !== void 0) {
		next.mode = "announce";
		hasPatch = true;
	}
	if (hints.channel ?? hints.provider) {
		next.channel = hints.channel ?? hints.provider;
		hasPatch = true;
	}
	if (hints.to) {
		next.to = hints.to;
		hasPatch = true;
	}
	if (hints.threadId) {
		next.threadId = hints.threadId;
		hasPatch = true;
	}
	if (hints.bestEffortDeliver !== void 0) {
		next.bestEffort = hints.bestEffortDeliver;
		hasPatch = true;
	}
	return hasPatch ? next : null;
}
/** Merge legacy payload delivery hints into an existing delivery object. */
function mergeLegacyDeliveryInto(delivery, payload) {
	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 ("threadId" in patch && patch.threadId !== next.threadId) {
		next.threadId = patch.threadId;
		mutated = true;
	}
	if ("bestEffort" in patch && patch.bestEffort !== next.bestEffort) {
		next.bestEffort = patch.bestEffort;
		mutated = true;
	}
	return {
		delivery: next,
		mutated
	};
}
/** Normalize delivery and strip consumed legacy delivery fields from the payload. */
function normalizeLegacyDeliveryInput(params) {
	if (!params.payload || !hasLegacyDeliveryHints(params.payload)) return {
		delivery: params.delivery ?? void 0,
		mutated: false
	};
	const nextDelivery = params.delivery ? mergeLegacyDeliveryInto(params.delivery, params.payload) : {
		delivery: buildDeliveryFromLegacyPayload(params.payload),
		mutated: true
	};
	stripLegacyDeliveryFields(params.payload);
	return {
		delivery: nextDelivery.delivery,
		mutated: true
	};
}
function stripLegacyDeliveryFields(payload) {
	if ("deliver" in payload) delete payload.deliver;
	if ("channel" in payload) delete payload.channel;
	if ("provider" in payload) delete payload.provider;
	if ("to" in payload) delete payload.to;
	if ("threadId" in payload) delete payload.threadId;
	if ("bestEffortDeliver" in payload) delete payload.bestEffortDeliver;
}
//#endregion
//#region src/commands/doctor/cron/payload-migration.ts
const LEGACY_AGENT_TURN_COMMAND_MARKER_RE = /\bCommand to run\s*:/iu;
const LEGACY_AGENT_TURN_COMMAND_FIELD_RE = /^\s*-\s*(command|workdir|timeout)\s*:\s*(.*?)\s*$/iu;
const SHELL_TOOL_NAMES = new Set([
	"bash",
	"command",
	"exec",
	"process",
	"shell",
	"sh"
]);
const SHELL_COMMAND_MESSAGE_RE = /\b(?:bash|command|execute|exec|process|run|shell)\b[\s\S]{0,240}\b(?:python3?|node|bun|pnpm|npm|npx|yarn|sh|bash|sudo|cd|\.\/|\/[A-Za-z0-9._/-]+)\b/iu;
const LEGACY_DELIVERY_HINT_FIELDS = [
	"deliver",
	"bestEffortDeliver",
	"channel",
	"provider",
	"to",
	"threadId"
];
function hasShellToolAccess(toolsAllow) {
	if (toolsAllow === void 0) return true;
	if (!Array.isArray(toolsAllow)) return false;
	return toolsAllow.some((tool) => {
		const normalized = normalizeOptionalLowercaseString(tool);
		return normalized === "*" || (normalized ? SHELL_TOOL_NAMES.has(normalized) : false);
	});
}
function toCanonicalOpenAIModelRef(value) {
	const raw = readStringValue(value);
	if (typeof raw !== "string") return;
	const trimmed = raw.trim();
	const slash = trimmed.indexOf("/");
	if (slash <= 0) return;
	if (trimmed.slice(0, slash).trim().toLowerCase() !== "openai-codex") return;
	const model = trimmed.slice(slash + 1).trim();
	return model ? `openai/${model}` : void 0;
}
function normalizeChannel(value) {
	return normalizeOptionalLowercaseString(value) ?? "";
}
function parsePositiveInteger(value) {
	const trimmed = value.trim();
	if (!/^\d+$/u.test(trimmed)) return;
	const parsed = Number.parseInt(trimmed, 10);
	return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
}
function readPositiveInteger(value) {
	return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
}
function parseLegacyAgentTurnCommandMessage(message) {
	if (!LEGACY_AGENT_TURN_COMMAND_MARKER_RE.test(message)) return null;
	let command = "";
	let cwd;
	let timeoutSeconds;
	for (const line of message.split(/\r?\n/u)) {
		const match = LEGACY_AGENT_TURN_COMMAND_FIELD_RE.exec(line);
		if (!match) continue;
		const key = match[1]?.toLowerCase();
		const value = match[2]?.trim() ?? "";
		if (key === "command" && value && !command) command = value;
		else if (key === "workdir" && value && !cwd) cwd = value;
		else if (key === "timeout" && value && timeoutSeconds === void 0) timeoutSeconds = parsePositiveInteger(value);
	}
	if (!command) return null;
	return {
		command,
		...cwd ? { cwd } : {},
		...timeoutSeconds ? { timeoutSeconds } : {}
	};
}
/** Return true when a cron payload contains legacy `openai-codex/*` model refs. */
function hasLegacyOpenAICodexCronModelRef(payload) {
	if (toCanonicalOpenAIModelRef(payload.model)) return true;
	const fallbacks = payload.fallbacks;
	return Array.isArray(fallbacks) && fallbacks.some((fallback) => toCanonicalOpenAIModelRef(fallback));
}
function migrateLegacyOpenAICodexModelRefs(payload) {
	let mutated = false;
	const model = toCanonicalOpenAIModelRef(payload.model);
	if (model && payload.model !== model) {
		payload.model = model;
		mutated = true;
	}
	const fallbacks = payload.fallbacks;
	if (Array.isArray(fallbacks)) {
		const next = fallbacks.map((fallback) => toCanonicalOpenAIModelRef(fallback) ?? fallback);
		if (next.some((fallback, index) => fallback !== fallbacks[index])) {
			payload.fallbacks = next;
			mutated = true;
		}
	}
	return mutated;
}
/** Normalize legacy cron payload channel/provider and model reference fields in place. */
function migrateLegacyCronPayload(payload) {
	let mutated = false;
	const channelValue = readStringValue(payload.channel);
	const providerValue = readStringValue(payload.provider);
	const nextChannel = typeof channelValue === "string" && channelValue.trim().length > 0 ? normalizeChannel(channelValue) : typeof providerValue === "string" && providerValue.trim().length > 0 ? normalizeChannel(providerValue) : "";
	if (nextChannel) {
		if (channelValue !== nextChannel) {
			payload.channel = nextChannel;
			mutated = true;
		}
	}
	if ("provider" in payload) {
		delete payload.provider;
		mutated = true;
	}
	if (migrateLegacyOpenAICodexModelRefs(payload)) mutated = true;
	return mutated;
}
function migrateLegacyAgentTurnCommandPayload(payload) {
	if (payload.kind !== "agentTurn") return false;
	const message = readStringValue(payload.message);
	if (typeof message !== "string") return false;
	const parsed = parseLegacyAgentTurnCommandMessage(message);
	if (!parsed) return false;
	if (!hasShellToolAccess(payload.toolsAllow)) return false;
	const timeoutSeconds = readPositiveInteger(payload.timeoutSeconds) ?? parsed.timeoutSeconds;
	const deliveryHints = {};
	for (const key of LEGACY_DELIVERY_HINT_FIELDS) if (key in payload) deliveryHints[key] = payload[key];
	for (const key of Object.keys(payload)) delete payload[key];
	payload.kind = "command";
	payload.argv = [
		"sh",
		"-lc",
		parsed.command
	];
	if (parsed.cwd) payload.cwd = parsed.cwd;
	if (timeoutSeconds !== void 0) payload.timeoutSeconds = timeoutSeconds;
	Object.assign(payload, deliveryHints);
	return true;
}
function hasUnresolvedAgentTurnShellToolPrompt(payload) {
	if (payload.kind !== "agentTurn") return false;
	const message = readStringValue(payload.message);
	if (typeof message !== "string") return false;
	const parsed = parseLegacyAgentTurnCommandMessage(message);
	return Boolean(parsed) || hasShellToolAccess(payload.toolsAllow) && SHELL_COMMAND_MESSAGE_RE.test(message);
}
//#endregion
//#region src/commands/doctor/cron/store-migration.ts
function incrementIssue(issues, key) {
	issues[key] = (issues[key] ?? 0) + 1;
}
function normalizeStoredCronJobIdentity(raw) {
	const hadIdKey = "id" in raw;
	const hadJobIdKey = "jobId" in raw;
	const id = normalizeOptionalStringifiedId(raw.id);
	const legacyJobId = normalizeOptionalStringifiedId(raw.jobId);
	const canonicalId = id ?? legacyJobId ?? `cron-${randomUUID()}`;
	const nonStringIdIssue = hadIdKey && raw.id != null && typeof raw.id !== "string";
	const missingIdIssue = !id && !legacyJobId;
	let mutated = false;
	if (raw.id !== canonicalId) {
		raw.id = canonicalId;
		mutated = true;
	}
	if (hadJobIdKey) {
		delete raw.jobId;
		mutated = true;
	}
	return {
		mutated,
		legacyJobIdIssue: hadJobIdKey,
		missingIdIssue,
		nonStringIdIssue
	};
}
function normalizePayloadKind(payload) {
	const raw = normalizeOptionalLowercaseString(payload.kind) ?? "";
	if (raw === "agentturn") {
		if (payload.kind !== "agentTurn") {
			payload.kind = "agentTurn";
			return true;
		}
		return false;
	}
	if (raw === "systemevent") {
		if (payload.kind !== "systemEvent") {
			payload.kind = "systemEvent";
			return true;
		}
		return false;
	}
	return false;
}
function inferPayloadIfMissing(raw) {
	const message = normalizeOptionalString(raw.message) ?? "";
	const text = normalizeOptionalString(raw.text) ?? "";
	const command = normalizeOptionalString(raw.command) ?? "";
	if (message) {
		raw.payload = {
			kind: "agentTurn",
			message
		};
		return true;
	}
	if (text) {
		raw.payload = {
			kind: "systemEvent",
			text
		};
		return true;
	}
	if (command) {
		raw.payload = {
			kind: "systemEvent",
			text: command
		};
		return true;
	}
	return false;
}
function copyTopLevelAgentTurnFields(raw, payload) {
	let mutated = false;
	const copyTrimmedString = (field) => {
		if (normalizeOptionalString(payload[field])) return;
		const value = normalizeOptionalString(raw[field]);
		if (value) {
			payload[field] = value;
			mutated = true;
		}
	};
	copyTrimmedString("model");
	copyTrimmedString("thinking");
	if (typeof payload.timeoutSeconds !== "number" && typeof raw.timeoutSeconds === "number" && Number.isFinite(raw.timeoutSeconds)) {
		payload.timeoutSeconds = Math.max(0, Math.floor(raw.timeoutSeconds));
		mutated = true;
	}
	if (typeof payload.allowUnsafeExternalContent !== "boolean" && typeof raw.allowUnsafeExternalContent === "boolean") {
		payload.allowUnsafeExternalContent = raw.allowUnsafeExternalContent;
		mutated = true;
	}
	if (typeof payload.deliver !== "boolean" && typeof raw.deliver === "boolean") {
		payload.deliver = raw.deliver;
		mutated = true;
	}
	const channel = normalizeOptionalString(raw.channel);
	if (typeof payload.channel !== "string" && channel) {
		payload.channel = channel;
		mutated = true;
	}
	const to = normalizeOptionalString(raw.to);
	if (typeof payload.to !== "string" && to) {
		payload.to = to;
		mutated = true;
	}
	const rawThreadId = normalizeOptionalString(raw.threadId);
	if (!("threadId" in payload) && (typeof raw.threadId === "number" && Number.isFinite(raw.threadId) || Boolean(rawThreadId))) {
		payload.threadId = rawThreadId ?? raw.threadId;
		mutated = true;
	}
	if (typeof payload.bestEffortDeliver !== "boolean" && typeof raw.bestEffortDeliver === "boolean") {
		payload.bestEffortDeliver = raw.bestEffortDeliver;
		mutated = true;
	}
	const provider = normalizeOptionalString(raw.provider);
	if (typeof payload.provider !== "string" && provider) {
		payload.provider = provider;
		mutated = true;
	}
	return mutated;
}
function stripLegacyTopLevelFields(raw) {
	if ("model" in raw) delete raw.model;
	if ("thinking" in raw) delete raw.thinking;
	if ("timeoutSeconds" in raw) delete raw.timeoutSeconds;
	if ("allowUnsafeExternalContent" in raw) delete raw.allowUnsafeExternalContent;
	if ("message" in raw) delete raw.message;
	if ("text" in raw) delete raw.text;
	if ("deliver" in raw) delete raw.deliver;
	if ("channel" in raw) delete raw.channel;
	if ("to" in raw) delete raw.to;
	if ("threadId" in raw) delete raw.threadId;
	if ("bestEffortDeliver" in raw) delete raw.bestEffortDeliver;
	if ("provider" in raw) delete raw.provider;
	if ("command" in raw) delete raw.command;
	if ("timeout" in raw) delete raw.timeout;
}
/** Normalize persisted cron jobs in place and report issues plus rows to quarantine. */
function normalizeStoredCronJobs(jobs) {
	const issues = {};
	const unresolvedAgentTurnShellToolPromptJobs = [];
	let mutated = false;
	const keptJobs = [];
	const removedJobs = [];
	for (const [sourceIndex, raw] of jobs.entries()) {
		const jobIssues = /* @__PURE__ */ new Set();
		const trackIssue = (key) => {
			if (jobIssues.has(key)) return;
			jobIssues.add(key);
			incrementIssue(issues, key);
		};
		const state = raw.state;
		if (!state || typeof state !== "object" || Array.isArray(state)) {
			raw.state = {};
			mutated = true;
		}
		const idNorm = normalizeStoredCronJobIdentity(raw);
		if (idNorm.mutated) mutated = true;
		if (idNorm.legacyJobIdIssue) trackIssue("jobId");
		if (idNorm.missingIdIssue) trackIssue("missingId");
		if (idNorm.nonStringIdIssue) trackIssue("nonStringId");
		if (typeof raw.schedule === "string") {
			raw.schedule = {
				kind: "cron",
				expr: raw.schedule.trim()
			};
			mutated = true;
			trackIssue("legacyScheduleString");
		}
		const nameRaw = raw.name;
		if (typeof nameRaw !== "string" || nameRaw.trim().length === 0) {
			raw.name = inferCronJobName({
				schedule: raw.schedule,
				payload: raw.payload
			});
			mutated = true;
		} else raw.name = nameRaw.trim();
		const desc = normalizeOptionalString(raw.description);
		if (raw.description !== desc) {
			raw.description = desc;
			mutated = true;
		}
		if ("sessionKey" in raw) {
			const sessionKey = typeof raw.sessionKey === "string" ? normalizeOptionalString(raw.sessionKey) : void 0;
			if (raw.sessionKey !== sessionKey) {
				raw.sessionKey = sessionKey;
				mutated = true;
			}
		}
		if (typeof raw.enabled !== "boolean") {
			raw.enabled = true;
			mutated = true;
		}
		const wakeModeRaw = normalizeOptionalLowercaseString(raw.wakeMode) ?? "";
		if (wakeModeRaw === "next-heartbeat") {
			if (raw.wakeMode !== "next-heartbeat") {
				raw.wakeMode = "next-heartbeat";
				mutated = true;
			}
		} else if (wakeModeRaw === "now") {
			if (raw.wakeMode !== "now") {
				raw.wakeMode = "now";
				mutated = true;
			}
		} else {
			raw.wakeMode = "now";
			mutated = true;
		}
		const payload = raw.payload;
		if ((!payload || typeof payload !== "object" || Array.isArray(payload)) && inferPayloadIfMissing(raw)) {
			mutated = true;
			trackIssue("legacyTopLevelPayloadFields");
		}
		const payloadRecord = raw.payload && typeof raw.payload === "object" && !Array.isArray(raw.payload) ? raw.payload : null;
		if (payloadRecord) {
			if (normalizePayloadKind(payloadRecord)) {
				mutated = true;
				trackIssue("legacyPayloadKind");
			}
			if (!payloadRecord.kind) {
				if (normalizeOptionalString(payloadRecord.message)) {
					payloadRecord.kind = "agentTurn";
					mutated = true;
					trackIssue("legacyPayloadKind");
				} else if (normalizeOptionalString(payloadRecord.text)) {
					payloadRecord.kind = "systemEvent";
					mutated = true;
					trackIssue("legacyPayloadKind");
				}
			}
			if (payloadRecord.kind === "agentTurn" && copyTopLevelAgentTurnFields(raw, payloadRecord)) mutated = true;
			if (payloadRecord.kind === "systemEvent" && !normalizeOptionalString(payloadRecord.text)) {
				const message = normalizeOptionalString(payloadRecord.message);
				if (message) {
					payloadRecord.text = message;
					delete payloadRecord.message;
					mutated = true;
					trackIssue("legacyPayloadKind");
				}
			}
		}
		const hadLegacyTopLevelPayloadFields = "model" in raw || "thinking" in raw || "timeoutSeconds" in raw || "allowUnsafeExternalContent" in raw || "message" in raw || "text" in raw || "command" in raw || "timeout" in raw;
		const hadLegacyTopLevelDeliveryFields = "deliver" in raw || "channel" in raw || "to" in raw || "threadId" in raw || "bestEffortDeliver" in raw || "provider" in raw;
		if (hadLegacyTopLevelPayloadFields || hadLegacyTopLevelDeliveryFields) {
			stripLegacyTopLevelFields(raw);
			mutated = true;
			if (hadLegacyTopLevelPayloadFields) trackIssue("legacyTopLevelPayloadFields");
			if (hadLegacyTopLevelDeliveryFields) trackIssue("legacyTopLevelDeliveryFields");
		}
		if (payloadRecord) {
			const hadLegacyPayloadProvider = Boolean(normalizeOptionalString(payloadRecord.provider));
			const hadLegacyPayloadCodexModel = hasLegacyOpenAICodexCronModelRef(payloadRecord);
			if (migrateLegacyCronPayload(payloadRecord)) {
				mutated = true;
				if (hadLegacyPayloadCodexModel) trackIssue("legacyPayloadCodexModel");
				if (hadLegacyPayloadProvider) trackIssue("legacyPayloadProvider");
			}
			if (migrateLegacyAgentTurnCommandPayload(payloadRecord)) {
				mutated = true;
				trackIssue("legacyAgentTurnCommandPayload");
			} else if (hasUnresolvedAgentTurnShellToolPrompt(payloadRecord)) {
				trackIssue("unresolvedAgentTurnShellToolPrompt");
				const name = normalizeOptionalString(raw.name) ?? normalizeOptionalString(raw.id);
				if (name) unresolvedAgentTurnShellToolPromptJobs.push(name);
			}
		}
		const schedule = raw.schedule;
		if (schedule && typeof schedule === "object" && !Array.isArray(schedule)) {
			const sched = schedule;
			const kind = normalizeOptionalLowercaseString(sched.kind) ?? "";
			if (!kind && ("at" in sched || "atMs" in sched)) {
				sched.kind = "at";
				mutated = true;
			}
			const atRaw = normalizeOptionalString(sched.at) ?? "";
			const atMsRaw = sched.atMs;
			const parsedAtMs = typeof atMsRaw === "number" ? atMsRaw : typeof atMsRaw === "string" ? parseAbsoluteTimeMs(atMsRaw) : atRaw ? parseAbsoluteTimeMs(atRaw) : null;
			const parsedAt = parsedAtMs !== null ? timestampMsToIsoString(parsedAtMs) : void 0;
			const fallbackAtMs = !parsedAt && atRaw ? parseAbsoluteTimeMs(atRaw) : null;
			const fallbackAt = fallbackAtMs !== null ? timestampMsToIsoString(fallbackAtMs) : void 0;
			const normalizedAt = parsedAt ?? fallbackAt;
			if (normalizedAt) {
				sched.at = normalizedAt;
				if ("atMs" in sched) delete sched.atMs;
				mutated = true;
			}
			const everyMsRaw = sched.everyMs;
			const everyMsCoerced = coerceFiniteScheduleNumber(everyMsRaw);
			const everyMs = everyMsCoerced !== void 0 ? Math.floor(everyMsCoerced) : null;
			if (everyMs !== null && everyMsRaw !== everyMs) {
				sched.everyMs = everyMs;
				mutated = true;
			}
			if ((kind === "every" || sched.kind === "every") && everyMs !== null) {
				const anchorRaw = sched.anchorMs;
				const anchorCoerced = coerceFiniteScheduleNumber(anchorRaw);
				const normalizedAnchor = anchorCoerced !== void 0 ? Math.max(0, Math.floor(anchorCoerced)) : typeof raw.createdAtMs === "number" && Number.isFinite(raw.createdAtMs) ? Math.max(0, Math.floor(raw.createdAtMs)) : typeof raw.updatedAtMs === "number" && Number.isFinite(raw.updatedAtMs) ? Math.max(0, Math.floor(raw.updatedAtMs)) : null;
				if (normalizedAnchor !== null && anchorRaw !== normalizedAnchor) {
					sched.anchorMs = normalizedAnchor;
					mutated = true;
				}
			}
			const exprRaw = normalizeOptionalString(sched.expr) ?? "";
			const legacyCronRaw = normalizeOptionalString(sched.cron) ?? "";
			let normalizedExpr = exprRaw;
			if (!normalizedExpr && legacyCronRaw) {
				normalizedExpr = legacyCronRaw;
				sched.expr = normalizedExpr;
				mutated = true;
				trackIssue("legacyScheduleCron");
			}
			if (typeof sched.expr === "string" && sched.expr !== normalizedExpr) {
				sched.expr = normalizedExpr;
				mutated = true;
			}
			if ("cron" in sched) {
				delete sched.cron;
				mutated = true;
				trackIssue("legacyScheduleCron");
			}
			if ((kind === "cron" || sched.kind === "cron") && normalizedExpr) {
				const explicitStaggerMs = normalizeCronStaggerMs(sched.staggerMs);
				const defaultStaggerMs = resolveDefaultCronStaggerMs(normalizedExpr);
				const targetStaggerMs = explicitStaggerMs ?? defaultStaggerMs;
				if (targetStaggerMs === void 0) {
					if ("staggerMs" in sched) {
						delete sched.staggerMs;
						mutated = true;
					}
				} else if (sched.staggerMs !== targetStaggerMs) {
					sched.staggerMs = targetStaggerMs;
					mutated = true;
				}
			}
		}
		const delivery = raw.delivery;
		if (delivery && typeof delivery === "object" && !Array.isArray(delivery)) {
			const modeRaw = delivery.mode;
			if (typeof modeRaw === "string") {
				if ((normalizeOptionalLowercaseString(modeRaw) ?? "") === "deliver") {
					delivery.mode = "announce";
					mutated = true;
					trackIssue("legacyDeliveryMode");
				}
			} else if (modeRaw === void 0 || modeRaw === null) {
				delivery.mode = "announce";
				mutated = true;
			}
		}
		const isolation = raw.isolation;
		if (isolation && typeof isolation === "object" && !Array.isArray(isolation)) {
			delete raw.isolation;
			mutated = true;
		}
		const payloadKind = payloadRecord && typeof payloadRecord.kind === "string" ? payloadRecord.kind : "";
		const rawSessionTarget = normalizeOptionalString(raw.sessionTarget) ?? "";
		const loweredSessionTarget = normalizeLowercaseStringOrEmpty(rawSessionTarget);
		if (loweredSessionTarget === "main" || loweredSessionTarget === "isolated") {
			if (raw.sessionTarget !== loweredSessionTarget) {
				raw.sessionTarget = loweredSessionTarget;
				mutated = true;
			}
		} else if (loweredSessionTarget.startsWith("session:")) {
			const customSessionId = rawSessionTarget.slice(8).trim();
			if (customSessionId) {
				const normalizedSessionTarget = `session:${customSessionId}`;
				if (raw.sessionTarget !== normalizedSessionTarget) {
					raw.sessionTarget = normalizedSessionTarget;
					mutated = true;
				}
			}
		} else if (loweredSessionTarget === "current") {
			if (raw.sessionTarget !== "isolated") {
				raw.sessionTarget = "isolated";
				mutated = true;
			}
		} else {
			const inferredSessionTarget = payloadKind === "agentTurn" || payloadKind === "command" ? "isolated" : "main";
			if (raw.sessionTarget !== inferredSessionTarget) {
				raw.sessionTarget = inferredSessionTarget;
				mutated = true;
			}
		}
		const sessionTarget = normalizeOptionalLowercaseString(raw.sessionTarget) ?? "";
		const isIsolatedRunnablePayload = sessionTarget === "isolated" || sessionTarget === "current" || sessionTarget.startsWith("session:") || sessionTarget === "" && (payloadKind === "agentTurn" || payloadKind === "command");
		const hasDelivery = delivery && typeof delivery === "object" && !Array.isArray(delivery);
		const normalizedLegacy = normalizeLegacyDeliveryInput({
			delivery: hasDelivery ? delivery : null,
			payload: payloadRecord
		});
		if (isIsolatedRunnablePayload && (payloadKind === "agentTurn" || payloadKind === "command")) {
			if (!hasDelivery && normalizedLegacy.delivery) {
				raw.delivery = normalizedLegacy.delivery;
				mutated = true;
			} else if (!hasDelivery) {
				raw.delivery = { mode: "announce" };
				mutated = true;
			} else if (normalizedLegacy.mutated && normalizedLegacy.delivery) {
				raw.delivery = normalizedLegacy.delivery;
				mutated = true;
			}
		} else if (normalizedLegacy.mutated && normalizedLegacy.delivery) {
			raw.delivery = normalizedLegacy.delivery;
			mutated = true;
		}
		const invalidPersistedReason = getInvalidPersistedCronJobReason(raw);
		if (invalidPersistedReason === "missing-schedule" || invalidPersistedReason === "invalid-schedule") {
			trackIssue("invalidSchedule");
			removedJobs.push({
				job: structuredClone(raw),
				reason: invalidPersistedReason,
				sourceIndex
			});
			mutated = true;
			continue;
		}
		if (invalidPersistedReason === "missing-payload" || invalidPersistedReason === "invalid-payload") {
			trackIssue("invalidPayload");
			removedJobs.push({
				job: structuredClone(raw),
				reason: invalidPersistedReason,
				sourceIndex
			});
			mutated = true;
			continue;
		}
		keptJobs.push(raw);
	}
	if (keptJobs.length !== jobs.length) jobs.splice(0, jobs.length, ...keptJobs);
	return {
		issues,
		unresolvedAgentTurnShellToolPromptJobs,
		jobs,
		mutated,
		removedJobs
	};
}
//#endregion
//#region src/commands/doctor/cron/warnings.ts
const execFileAsync = promisify(execFile);
const LEGACY_WHATSAPP_HEALTH_SCRIPT_RE = /(?:^|\s)(?:"[^"]*ensure-whatsapp\.sh"|'[^']*ensure-whatsapp\.sh'|[^\s#;|&]*ensure-whatsapp\.sh)\b/u;
const CRON_MODEL_OVERRIDE_EXAMPLE_LIMIT = 3;
function pluralize$1(count, noun) {
	return `${count} ${noun}${count === 1 ? "" : "s"}`;
}
function normalizeModelProvider(value) {
	const raw = normalizeOptionalString(value);
	if (!raw) return;
	const slash = raw.indexOf("/");
	if (slash <= 0 || slash >= raw.length - 1) return;
	return raw.slice(0, slash).trim().toLowerCase() || void 0;
}
function normalizeModelRef(value) {
	const raw = normalizeOptionalString(value);
	if (!raw) return;
	const slash = raw.indexOf("/");
	if (slash <= 0 || slash >= raw.length - 1) return;
	const provider = raw.slice(0, slash).trim().toLowerCase();
	const model = raw.slice(slash + 1).trim();
	return provider && model ? `${provider}/${model}` : void 0;
}
function normalizeModelMismatchKey(value) {
	return normalizeModelRef(value) ?? normalizeOptionalString(value)?.toLowerCase();
}
function getRecord(value) {
	return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function formatProviderCounts(counts) {
	return [...counts.entries()].toSorted(([left], [right]) => left.localeCompare(right)).map(([provider, count]) => `${provider}=${count}`).join(", ");
}
/** Emit a note when cron jobs pin models instead of inheriting the default model. */
function noteCronModelOverrides(params) {
	const defaultModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model);
	const defaultKey = normalizeModelMismatchKey(defaultModel);
	const providerCounts = /* @__PURE__ */ new Map();
	const mismatchExamples = [];
	let overrideCount = 0;
	let mismatchCount = 0;
	for (const rawJob of params.jobs) {
		const payload = getRecord(rawJob.payload);
		const kind = normalizeOptionalString(payload?.kind)?.toLowerCase();
		if (kind && kind !== "agentturn") continue;
		const model = normalizeOptionalString(payload?.model);
		if (!model) continue;
		overrideCount += 1;
		const provider = normalizeModelProvider(model) ?? "bare/alias";
		providerCounts.set(provider, (providerCounts.get(provider) ?? 0) + 1);
		const modelKey = normalizeModelMismatchKey(model);
		if (defaultKey && modelKey && modelKey !== defaultKey) {
			mismatchCount += 1;
			if (mismatchExamples.length < CRON_MODEL_OVERRIDE_EXAMPLE_LIMIT) {
				const id = normalizeOptionalString(rawJob.id) ?? normalizeOptionalString(rawJob.jobId);
				const name = normalizeOptionalString(rawJob.name);
				mismatchExamples.push(`${id ?? name ?? "<unnamed>"} -> ${model}`);
			}
		}
	}
	if (overrideCount === 0) return;
	const lines = [
		`Cron model overrides detected at ${shortenHomePath(params.storePath)}.`,
		`- ${pluralize$1(overrideCount, "job")} set \`payload.model\` and will not inherit \`agents.defaults.model\`${defaultModel ? ` (${defaultModel})` : ""}`,
		`- Provider namespaces: ${formatProviderCounts(providerCounts)}`
	];
	if (mismatchCount > 0) {
		lines.push(`- ${pluralize$1(mismatchCount, "job")} ${mismatchCount === 1 ? "uses" : "use"} a different model than \`agents.defaults.model\`${defaultModel ? ` (${defaultModel})` : ""}`);
		lines.push(`- Examples: ${mismatchExamples.join(", ")}`);
	}
	lines.push(`Review with ${formatCliCommand("openclaw cron list")} and ${formatCliCommand("openclaw cron show <job-id>")}; remove \`payload.model\` from jobs that should inherit the default.`);
	note(lines.join("\n"), "Cron");
}
async function readUserCrontab() {
	const result = await execFileAsync("crontab", ["-l"], {
		encoding: "utf8",
		windowsHide: true
	});
	return {
		stdout: result.stdout,
		stderr: result.stderr
	};
}
function coerceCrontabText(crontab) {
	if (typeof crontab === "string") return crontab;
	if (crontab == null) return "";
	if (typeof crontab === "number" || typeof crontab === "boolean" || typeof crontab === "bigint") return String(crontab);
	return "";
}
function findLegacyWhatsAppHealthCrontabLines(crontab) {
	return coerceCrontabText(crontab).split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).filter((line) => LEGACY_WHATSAPP_HEALTH_SCRIPT_RE.test(line));
}
/** Return a warning when the user's crontab still runs the old WhatsApp health script. */
async function collectLegacyWhatsAppCrontabHealthWarning(params = {}) {
	if ((params.platform ?? process.platform) !== "linux") return null;
	let crontab;
	try {
		crontab = (await (params.readCrontab ?? readUserCrontab)()).stdout;
	} catch {
		return null;
	}
	const legacyLines = findLegacyWhatsAppHealthCrontabLines(crontab);
	if (legacyLines.length === 0) return null;
	return [
		"Legacy WhatsApp crontab health check detected.",
		"`~/.openclaw/bin/ensure-whatsapp.sh` is not maintained by current OpenClaw and can misreport `Gateway inactive` from cron when the systemd user bus environment is missing.",
		`Remove the stale crontab entry with ${formatCliCommand("crontab -e")}; use ${formatCliCommand("openclaw channels status --probe")}, ${formatCliCommand("openclaw doctor")}, and ${formatCliCommand("openclaw gateway status")} for current health checks.`,
		`Matched ${pluralize$1(legacyLines.length, "entry")}.`
	].join("\n");
}
/** Emit the legacy WhatsApp crontab warning when present. */
async function noteLegacyWhatsAppCrontabHealthCheck(params = {}) {
	const warning = await collectLegacyWhatsAppCrontabHealthWarning(params);
	if (warning) note(warning, "Cron");
}
//#endregion
//#region src/commands/doctor/cron/index.ts
function pluralize(count, noun) {
	return `${count} ${noun}${count === 1 ? "" : "s"}`;
}
function formatRunLogMigrationNote(importedFiles) {
	return importedFiles > 0 ? ` Imported ${pluralize(importedFiles, "legacy cron run log")} into SQLite.` : "";
}
function errorMessage(err) {
	return err instanceof Error ? err.message : String(err);
}
async function loadLegacyCronRepairState(params) {
	const storePath = resolveCronJobsStorePath(params.cfg.cron?.store);
	const quarantinePath = resolveCronQuarantinePath(storePath);
	const legacyStoreDetected = await legacyCronStoreFilesExist(storePath);
	const legacyRunLogDetected = await legacyCronRunLogFilesExist(storePath);
	if (params.onlyIfLegacyDetected && !legacyStoreDetected && !legacyRunLogDetected) return null;
	const loaded = await loadCronJobsStoreWithConfigJobs(storePath);
	const currentJobs = loaded.configJobs.length > 0 ? loaded.configJobs.map((job, index) => mergeRuntimeEntryIntoConfigJob({
		job,
		runtimeEntry: loaded.configJobRuntimeEntries[index]
	})) : loaded.store.jobs;
	const sqliteProjectionBackfillCount = loaded.configJobs.length > 0 ? currentJobs.filter((job, index) => needsSqliteProjectionBackfill({
		configJob: job,
		projectedJob: loaded.store.jobs[index]
	})).length : 0;
	let rawJobs = currentJobs;
	let legacyImportCount = 0;
	if (legacyStoreDetected) {
		const legacyStore = (await loadLegacyCronStoreForMigration(storePath)).store;
		const merged = mergeLegacyCronJobs({
			currentJobs: rawJobs,
			legacyJobs: legacyStore.jobs
		});
		rawJobs = merged.jobs;
		legacyImportCount = merged.importedCount;
	}
	return {
		storePath,
		quarantinePath,
		legacyStoreDetected,
		legacyRunLogDetected,
		legacyImportCount,
		sqliteProjectionBackfillCount,
		rawJobs
	};
}
async function applyLegacyCronStoreRepair(params) {
	const { state } = params;
	const changes = [];
	const warnings = [];
	const normalized = params.normalized ?? normalizeStoredCronJobs(state.rawJobs);
	const legacyWebhook = normalizeOptionalString(params.cfg.cron?.webhook);
	const notifyMigration = migrateLegacyNotifyFallback({
		jobs: state.rawJobs,
		legacyWebhook
	});
	const dreamingMigration = migrateLegacyDreamingPayloadShape(state.rawJobs);
	warnings.push(...notifyMigration.warnings);
	const changed = state.legacyStoreDetected || state.legacyRunLogDetected || state.sqliteProjectionBackfillCount > 0 || normalized.mutated || notifyMigration.changed || dreamingMigration.changed;
	if (!changed && warnings.length === 0) return {
		changes,
		warnings
	};
	if (changed) try {
		if (normalized.removedJobs.length > 0) await saveCronQuarantineFile({
			storePath: state.storePath,
			nowMs: Date.now(),
			entries: normalized.removedJobs.map((entry) => ({
				sourceIndex: entry.sourceIndex,
				reason: entry.reason,
				job: entry.job
			}))
		});
		await saveCronJobsStore(state.storePath, {
			version: 1,
			jobs: state.rawJobs
		});
	} catch (err) {
		return {
			changes,
			warnings: [...warnings, `Failed writing migrated cron store at ${shortenHomePath(state.storePath)}: ${errorMessage(err)}`]
		};
	}
	let importedRunLogs = 0;
	if (state.legacyRunLogDetected) try {
		importedRunLogs = (await migrateLegacyCronRunLogsToSqlite(state.storePath)).importedFiles;
	} catch (err) {
		warnings.push(`Failed importing legacy cron run logs at ${shortenHomePath(state.storePath)}: ${errorMessage(err)}`);
	}
	if (state.legacyStoreDetected) {
		await archiveLegacyCronStoreForMigration(state.storePath);
		changes.push(`Cron store migrated to SQLite at ${shortenHomePath(state.storePath)}.${formatRunLogMigrationNote(importedRunLogs)}`);
	} else if (state.legacyRunLogDetected && importedRunLogs > 0) changes.push(`Cron run logs migrated to SQLite at ${shortenHomePath(state.storePath)}.${formatRunLogMigrationNote(importedRunLogs)}`);
	else if (changed) changes.push(`Cron store normalized at ${shortenHomePath(state.storePath)}.`);
	if (dreamingMigration.rewrittenCount > 0) changes.push(`Rewrote ${pluralize(dreamingMigration.rewrittenCount, "managed dreaming job")} to run as an isolated agent turn so dreaming no longer requires heartbeat.`);
	return {
		changes,
		warnings
	};
}
async function repairLegacyCronStoreWithoutPrompt(params) {
	const storePath = resolveCronJobsStorePath(params.cfg.cron?.store);
	let state;
	try {
		state = await loadLegacyCronRepairState({
			cfg: params.cfg,
			onlyIfLegacyDetected: true
		});
	} catch (err) {
		return {
			changes: [],
			warnings: [`Failed reading legacy cron storage at ${shortenHomePath(storePath)}: ${errorMessage(err)}`]
		};
	}
	if (!state) return {
		changes: [],
		warnings: []
	};
	return await applyLegacyCronStoreRepair({
		cfg: params.cfg,
		state
	});
}
function noteLegacyCronRepairResult(result) {
	if (result.changes.length > 0) note(result.changes.join("\n"), "Doctor changes");
	if (result.warnings.length > 0) note(result.warnings.join("\n"), "Doctor warnings");
}
/** Inspect cron storage and optionally repair legacy JSON/SQLite/payload shapes. */
async function maybeRepairLegacyCronStore(params) {
	let state;
	try {
		state = await loadLegacyCronRepairState({ cfg: params.cfg });
	} catch (err) {
		const reason = err instanceof Error ? err.message : String(err);
		note([
			`Unable to read cron job store at ${shortenHomePath(resolveCronJobsStorePath(params.cfg.cron?.store))}.`,
			`- ${reason}`,
			`Fix the file's permissions or contents and re-run ${formatCliCommand("openclaw doctor")}; later health checks will continue.`
		].join("\n"), "Cron");
		return;
	}
	if (!state) return;
	const { storePath, quarantinePath, legacyStoreDetected, legacyRunLogDetected, legacyImportCount, sqliteProjectionBackfillCount, rawJobs } = state;
	try {
		const quarantine = await loadCronQuarantineFile(quarantinePath);
		if (quarantine.jobs.length > 0) note([
			`Quarantined cron job rows found at ${shortenHomePath(quarantinePath)}.`,
			`- ${pluralize(quarantine.jobs.length, "row")} was removed from the active cron store after runtime validation failed.`,
			`- Review or repair the quarantined rows manually before copying any job back into ${shortenHomePath(storePath)}.`
		].join("\n"), "Cron");
	} catch (err) {
		const reason = err instanceof Error ? err.message : String(err);
		note([`Unable to read quarantined cron rows at ${shortenHomePath(quarantinePath)}.`, `- ${reason}`].join("\n"), "Cron");
	}
	if (rawJobs.length === 0) {
		if (!legacyStoreDetected && !legacyRunLogDetected) return;
		const previewLines = [];
		if (legacyStoreDetected) previewLines.push("- legacy JSON cron store will be archived after SQLite migration");
		if (legacyRunLogDetected) previewLines.push("- legacy JSON cron run logs will be imported into SQLite");
		note([
			`Legacy cron storage detected at ${shortenHomePath(storePath)}.`,
			...previewLines,
			`Repair with ${formatCliCommand("openclaw doctor --fix")} to finish the migration.`
		].join("\n"), "Cron");
		if (!await params.prompter.confirm({
			message: "Repair legacy cron jobs now?",
			initialValue: true
		})) return;
		noteLegacyCronRepairResult(await applyLegacyCronStoreRepair({
			cfg: params.cfg,
			state
		}));
		return;
	}
	noteCronModelOverrides({
		cfg: params.cfg,
		jobs: rawJobs,
		storePath
	});
	const normalized = normalizeStoredCronJobs(rawJobs);
	const notifyCount = rawJobs.filter((job) => job.notify === true).length;
	const dreamingStaleCount = countStaleDreamingJobs(rawJobs);
	const previewLines = formatLegacyIssuePreview(normalized.issues, { unresolvedAgentTurnShellToolPrompt: normalized.unresolvedAgentTurnShellToolPromptJobs });
	if (legacyStoreDetected) previewLines.unshift(legacyImportCount > 0 ? `- ${pluralize(legacyImportCount, "legacy JSON cron job")} will be imported into SQLite` : "- legacy JSON cron store will be archived after SQLite migration");
	if (legacyRunLogDetected) previewLines.push("- legacy JSON cron run logs will be imported into SQLite");
	if (sqliteProjectionBackfillCount > 0) previewLines.push(`- ${pluralize(sqliteProjectionBackfillCount, "SQLite cron row")} will be backfilled from stored config JSON into split columns`);
	if (notifyCount > 0) previewLines.push(`- ${pluralize(notifyCount, "job")} still uses legacy \`notify: true\` webhook fallback`);
	if (dreamingStaleCount > 0) previewLines.push(`- ${pluralize(dreamingStaleCount, "managed dreaming job")} still has the legacy heartbeat-coupled shape`);
	if (previewLines.length === 0 && !legacyStoreDetected) return;
	note([
		`Legacy cron job storage detected at ${shortenHomePath(storePath)}.`,
		...previewLines,
		`Repair with ${formatCliCommand("openclaw doctor --fix")} to normalize the store before the next scheduler run.`
	].join("\n"), "Cron");
	if (!await params.prompter.confirm({
		message: "Repair legacy cron jobs now?",
		initialValue: true
	})) return;
	noteLegacyCronRepairResult(await applyLegacyCronStoreRepair({
		cfg: params.cfg,
		state,
		normalized
	}));
}
//#endregion
export { noteLegacyWhatsAppCrontabHealthCheck as i, repairLegacyCronStoreWithoutPrompt as n, collectLegacyWhatsAppCrontabHealthWarning as r, maybeRepairLegacyCronStore as t };