import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import "./src-BntaCZM-.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { t as safeParseJson } from "./json-coercion-ighRFv8Y.js";
import { f as asSafeIntegerInRange, x as parseStrictFiniteNumber } from "./number-coercion-CLj0HTDM.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { c as parseAgentSessionKey } from "./session-key-utils-dXMHZk48.js";
import { Jt as getNodeSqliteKysely, Kt as executeSqliteQuerySync, yt as normalizeSqliteNumber } from "./openclaw-state-db-Cb4e1jdd.js";
import { i as normalizeCronJobInput, t as parseCronPacingBounds } from "./pacing-OSxczKuT.js";
import { a as normalizeCronToolsAllowExecTarget, l as restoreCronPinnedExecGrant, o as normalizeCronToolsAllowExecTargetRequirement, u as stripCronPinnedExecGrant } from "./scheduled-tool-policy-B4xtJ5zX.js";
import { t as coerceFiniteScheduleNumber } from "./schedule-number-ZZmAUQRw.js";
import { n as normalizeCronStaggerMs } from "./types-CmTWyZpw.js";
import { n as getInvalidPersistedCronJobReason } from "./persisted-shape-Ct46BLwl.js";
//#region src/cron/normalize-job-identity.ts
/** Repairs legacy cron job identity fields into the canonical id shape. */
/** Normalizes mutable cron job rows from old `jobId` storage into the canonical `id` field. */
function normalizeCronJobIdentityFields(raw) {
	const rawId = normalizeOptionalString(raw.id) ?? "";
	const legacyJobId = normalizeOptionalString(raw.jobId) ?? "";
	const hadJobIdKey = "jobId" in raw;
	const normalizedId = rawId || legacyJobId;
	const idChanged = Boolean(normalizedId && raw.id !== normalizedId);
	if (idChanged) raw.id = normalizedId;
	if (hadJobIdKey) delete raw.jobId;
	return {
		mutated: idChanged || hadJobIdKey,
		legacyJobIdIssue: hadJobIdKey
	};
}
//#endregion
//#region src/cron/schedule-identity.ts
/** Builds stable identities for cron scheduling inputs. */
function readScheduleTime(record, key) {
	return coerceFiniteScheduleNumber(record[key]);
}
function readScheduleInteger(record, key) {
	return asSafeIntegerInRange(parseStrictFiniteNumber(record[key]), {
		min: Number.MIN_SAFE_INTEGER,
		max: Number.MAX_SAFE_INTEGER
	});
}
function schedulePayloadFromRecord(schedule) {
	const rawKind = normalizeOptionalString(schedule.kind)?.toLowerCase();
	const expr = normalizeOptionalString(schedule.expr);
	const at = normalizeOptionalString(schedule.at);
	const everyMs = readScheduleTime(schedule, "everyMs");
	const anchorMs = readScheduleTime(schedule, "anchorMs");
	const tz = normalizeOptionalString(schedule.tz);
	const staggerMs = normalizeCronStaggerMs(schedule.staggerMs);
	const kind = rawKind === "at" || rawKind === "every" || rawKind === "cron" || rawKind === "on-exit" || rawKind === "stream" ? rawKind : at ? "at" : everyMs !== void 0 ? "every" : expr ? "cron" : void 0;
	if (kind === "at") return at ? {
		kind: "at",
		at
	} : void 0;
	if (kind === "every" && everyMs !== void 0) return {
		kind: "every",
		everyMs,
		anchorMs
	};
	if (kind === "cron" && expr) return {
		kind: "cron",
		expr,
		tz,
		staggerMs
	};
	if (kind === "on-exit") {
		const command = normalizeOptionalString(schedule.command);
		return command ? {
			kind: "on-exit",
			command,
			cwd: normalizeOptionalString(schedule.cwd)
		} : void 0;
	}
	if (kind === "stream") {
		const command = schedule.command;
		if (!Array.isArray(command) || command.length === 0 || command.some((entry) => typeof entry !== "string" || entry.length === 0)) return;
		const mode = normalizeOptionalString(schedule.mode);
		return {
			kind: "stream",
			command: [...command],
			cwd: normalizeOptionalString(schedule.cwd),
			mode: mode === "line" || mode === "match" ? mode : void 0,
			match: typeof schedule.match === "string" ? schedule.match : void 0,
			batchMs: readScheduleInteger(schedule, "batchMs"),
			maxBatchBytes: readScheduleInteger(schedule, "maxBatchBytes")
		};
	}
}
function resolvePacingPayload(job) {
	if (job.pacing === void 0 || job.pacing === null) return;
	if (typeof job.pacing !== "object" || Array.isArray(job.pacing)) return null;
	const pacing = job.pacing;
	const min = normalizeOptionalString(pacing.min);
	const max = normalizeOptionalString(pacing.max);
	try {
		return parseCronPacingBounds({
			min,
			max
		});
	} catch {
		return null;
	}
}
/** Builds a stable scheduling identity for deciding whether stored timer state is still valid. */
function tryCronScheduleIdentity(job) {
	const schedule = job.schedule && typeof job.schedule === "object" && !Array.isArray(job.schedule) ? schedulePayloadFromRecord(job.schedule) : void 0;
	const pacing = resolvePacingPayload(job);
	if (!schedule || pacing === null) return;
	return JSON.stringify({
		version: 2,
		enabled: typeof job.enabled === "boolean" ? job.enabled : true,
		schedule,
		pacing,
		hasTrigger: job.trigger !== void 0 && job.trigger !== null
	});
}
/** Compares two cron jobs by the normalized inputs that affect next-run computation. */
function cronSchedulingInputsEqual(previous, next) {
	const previousIdentity = tryCronScheduleIdentity(previous);
	const nextIdentity = tryCronScheduleIdentity(next);
	return previousIdentity !== void 0 && previousIdentity === nextIdentity;
}
//#endregion
//#region src/cron/store/delivery-codec.ts
/** JSON codec for cron delivery configuration and explicit destination clears. */
const FAILURE_DESTINATION_FIELDS = [
	"channel",
	"to",
	"accountId",
	"mode"
];
/** Encodes explicitly undefined failure overrides as durable JSON null values. */
function deliveryToJson(delivery) {
	const failureDestination = delivery.failureDestination;
	if (!failureDestination) return { ...delivery };
	return {
		...delivery,
		failureDestination: Object.fromEntries(FAILURE_DESTINATION_FIELDS.filter((field) => Object.hasOwn(failureDestination, field)).map((field) => [field, failureDestination[field] ?? null]))
	};
}
/** Restores JSON null overrides as present-but-undefined runtime properties. */
function deliveryFromJson(value) {
	if (!isRecord(value) || value.mode !== "none" && value.mode !== "announce" && value.mode !== "webhook") return;
	const failureDestination = value.failureDestination;
	if (!isRecord(failureDestination)) return value;
	return {
		...value,
		failureDestination: Object.fromEntries(FAILURE_DESTINATION_FIELDS.filter((field) => Object.hasOwn(failureDestination, field)).map((field) => [field, failureDestination[field] ?? void 0]))
	};
}
//#endregion
//#region src/cron/store/scalar-codec.ts
function tryParseJsonObject(raw) {
	const parsed = safeParseJson(raw);
	return isRecord(parsed) ? parsed : void 0;
}
//#endregion
//#region src/cron/store/schema.ts
/** Creates the Kysely facade scoped to cron_jobs for synchronous SQLite access. */
function getCronStoreKysely(db) {
	return getNodeSqliteKysely(db);
}
//#endregion
//#region src/cron/store/row-codec.ts
function stripJobRuntimeFields(job) {
	const { runtimeAuthority: _runtimeAuthority, runtimeAuthorityRecoveryRequired: _runtimeAuthorityRecoveryRequired, state: _state, updatedAtMs: _updatedAtMs, ...rest } = job;
	const payload = isRecord(rest.payload) ? rest.payload : void 0;
	const storedToolsAllow = stripCronPinnedExecGrant({
		toolsAllow: Array.isArray(payload?.toolsAllow) ? payload.toolsAllow.filter((tool) => typeof tool === "string") : void 0,
		requirement: rest.toolsAllowExecTargetRequirement
	});
	return {
		...rest,
		...payload && storedToolsAllow ? { payload: {
			...payload,
			toolsAllow: storedToolsAllow
		} } : {},
		...rest.delivery ? { delivery: deliveryToJson(rest.delivery) } : {},
		state: {}
	};
}
function serializeCronJobState(state) {
	return JSON.stringify({
		...state,
		...state.lastRunStatus === void 0 && state.lastStatus !== void 0 ? { lastRunStatus: state.lastStatus } : {}
	});
}
function bindCronJobRow(storeKey, job, sortOrder) {
	return {
		store_key: storeKey,
		job_id: job.id,
		declaration_key: job.declarationKey ?? null,
		owner_agent_id: job.owner?.agentId ?? null,
		name: job.name,
		description: job.description ?? null,
		enabled: job.enabled ? 1 : 0,
		updated_at: job.updatedAtMs,
		agent_id: job.agentId ?? null,
		payload_kind: job.payload.kind,
		job_json: JSON.stringify(stripJobRuntimeFields(job)),
		state_json: serializeCronJobState(job.state ?? {}),
		runtime_updated_at_ms: job.updatedAtMs,
		schedule_identity: tryCronScheduleIdentity({ ...job }) ?? null,
		sort_order: sortOrder
	};
}
function normalizeCronJobForSqlite(job) {
	const raw = { ...structuredClone(job) };
	const hadDeleteAfterRun = Object.hasOwn(raw, "deleteAfterRun");
	normalizeCronJobIdentityFields(raw);
	const normalized = normalizeCronJobInput(raw, { applyDefaults: true });
	if (!normalized || getInvalidPersistedCronJobReason(normalized)) return null;
	if (!hadDeleteAfterRun) delete normalized.deleteAfterRun;
	const createdAtMs = typeof normalized.createdAtMs === "number" && Number.isFinite(normalized.createdAtMs) ? normalized.createdAtMs : Date.now();
	const updatedAtMs = typeof normalized.updatedAtMs === "number" && Number.isFinite(normalized.updatedAtMs) ? normalized.updatedAtMs : createdAtMs;
	return {
		...normalized,
		createdAtMs,
		updatedAtMs,
		state: isRecord(normalized.state) ? normalized.state : {}
	};
}
function countUnpersistableCronJobs(store) {
	return store.jobs.reduce((count, job) => count + (normalizeCronJobForSqlite(job) ? 0 : 1), 0);
}
/** Fails before replacing SQLite rows when any config job cannot round-trip. */
function assertCronStoreCanPersist(store) {
	const invalidJobs = countUnpersistableCronJobs(store);
	if (invalidJobs > 0) throw new Error(`Cannot persist cron store with ${invalidJobs} invalid job(s)`);
}
function decodeCronJobConfig(jobJson) {
	const delivery = deliveryFromJson(jobJson.delivery);
	return delivery ? {
		...jobJson,
		delivery
	} : jobJson;
}
function rowToCronJob(row, jobJson) {
	const state = tryParseJsonObject(row.state_json);
	if (!state || getInvalidPersistedCronJobReason(jobJson)) return null;
	const toolsAllowExecTarget = normalizeCronToolsAllowExecTarget(jobJson.toolsAllowExecTarget);
	const toolsAllowExecTargetRequirement = normalizeCronToolsAllowExecTargetRequirement(jobJson.toolsAllowExecTargetRequirement);
	const createdAtMs = typeof jobJson.createdAtMs === "number" && Number.isFinite(jobJson.createdAtMs) ? jobJson.createdAtMs : Date.now();
	const { notify: _legacyNotify, toolsAllowExecTarget: _rawToolsAllowExecTarget, toolsAllowExecTargetRequirement: _rawToolsAllowExecTargetRequirement, ...runtimeConfig } = decodeCronJobConfig(jobJson);
	const payload = isRecord(runtimeConfig.payload) ? runtimeConfig.payload : void 0;
	const runtimeToolsAllow = restoreCronPinnedExecGrant({
		toolsAllow: Array.isArray(payload?.toolsAllow) ? payload.toolsAllow.filter((tool) => typeof tool === "string") : void 0,
		requirement: toolsAllowExecTargetRequirement,
		execTarget: toolsAllowExecTarget
	});
	if (payload && runtimeToolsAllow) runtimeConfig.payload = {
		...payload,
		toolsAllow: runtimeToolsAllow
	};
	if (isRecord(runtimeConfig.delivery) && runtimeConfig.delivery.mode === void 0) runtimeConfig.delivery = deliveryFromJson({
		...runtimeConfig.delivery,
		mode: "announce"
	});
	return {
		...runtimeConfig,
		id: row.job_id,
		...toolsAllowExecTarget ? { toolsAllowExecTarget } : {},
		...toolsAllowExecTargetRequirement ? { toolsAllowExecTargetRequirement } : {},
		createdAtMs,
		updatedAtMs: normalizeSqliteNumber(row.runtime_updated_at_ms) ?? normalizeSqliteNumber(row.updated_at) ?? createdAtMs,
		state
	};
}
/** Projects a live job through the same normalization/codecs used by SQLite persistence. */
function projectCronJobThroughStorageCodec(job) {
	const normalized = normalizeCronJobForSqlite(job);
	if (!normalized) throw new Error(`cannot project invalid cron job ${job.id}`);
	const row = bindCronJobRow("config-revision", normalized, 0);
	const projected = rowToCronJob(row, tryParseJsonObject(row.job_json) ?? {});
	if (!projected) throw new Error(`cannot project cron job ${job.id} through storage codecs`);
	return projected;
}
/** Loads cron rows in config order with deterministic fallbacks for old rows. */
function loadCronRows(db, storeKey) {
	return executeSqliteQuerySync(db, getCronStoreKysely(db).selectFrom("cron_jobs").selectAll().where("store_key", "=", storeKey).orderBy("sort_order", "asc").orderBy("updated_at", "asc").orderBy("job_id", "asc")).rows;
}
/** Materializes retired ownership within the caller's write transaction. */
function materializeCronRowAgentOwners(db, storeKey, legacyDefaultAgentId) {
	const agentId = normalizeAgentId(legacyDefaultAgentId);
	let rewritten = 0;
	for (const row of loadCronRows(db, storeKey)) {
		const jobJson = tryParseJsonObject(row.job_json);
		const jsonSessionAgentId = parseAgentSessionKey(normalizeOptionalString(jobJson?.sessionKey))?.agentId;
		if (normalizeOptionalString(row.agent_id) || normalizeOptionalString(jobJson?.agentId) || jsonSessionAgentId) continue;
		if (jobJson) jobJson.agentId = agentId;
		executeSqliteQuerySync(db, getCronStoreKysely(db).updateTable("cron_jobs").set({
			agent_id: agentId,
			...jobJson ? { job_json: JSON.stringify(jobJson) } : {}
		}).where("store_key", "=", storeKey).where("job_id", "=", row.job_id));
		rewritten += 1;
	}
	return rewritten;
}
/** Removes one owned job family from obsolete store partitions. */
function deleteStaleCronJobFamilyRows(db, activeStoreKey, family) {
	const staleRows = executeSqliteQuerySync(db, getCronStoreKysely(db).selectFrom("cron_jobs").select([
		"store_key",
		"job_id",
		"declaration_key",
		"name",
		"description"
	]).where("store_key", "!=", activeStoreKey)).rows.filter((row) => row.declaration_key === family.declarationKey || row.name === family.name && row.description?.includes(family.ownerPluginTag) === true);
	for (const row of staleRows) {
		executeSqliteQuerySync(db, getCronStoreKysely(db).deleteFrom("cron_job_scratch").where("store_key", "=", row.store_key).where("job_id", "=", row.job_id));
		executeSqliteQuerySync(db, getCronStoreKysely(db).deleteFrom("cron_jobs").where("store_key", "=", row.store_key).where("job_id", "=", row.job_id));
	}
	return staleRows.length;
}
/** Replaces all persisted cron rows and returns the canonical jobs that were written. */
function replaceCronRows(db, storeKey, store) {
	const existingRows = executeSqliteQuerySync(db, getCronStoreKysely(db).selectFrom("cron_jobs").select("job_id").where("store_key", "=", storeKey)).rows;
	const normalizedJobs = [];
	for (const [index, job] of store.jobs.entries()) normalizedJobs.push(upsertCronJobRow(db, storeKey, job, index));
	const nextJobIds = new Set(normalizedJobs.map((job) => job.id));
	for (const row of existingRows) {
		if (nextJobIds.has(row.job_id)) continue;
		executeSqliteQuerySync(db, getCronStoreKysely(db).deleteFrom("cron_jobs").where("store_key", "=", storeKey).where("job_id", "=", row.job_id));
	}
	return normalizedJobs;
}
/** Upserts one persisted cron row without rewriting unrelated jobs in its store partition. */
function upsertCronJobRow(db, storeKey, job, sortOrder) {
	const normalized = normalizeCronJobForSqlite(job);
	if (!normalized) throw new Error(`Cannot persist invalid cron job ${job.id}`);
	const values = bindCronJobRow(storeKey, normalized, sortOrder);
	executeSqliteQuerySync(db, getCronStoreKysely(db).insertInto("cron_jobs").values(values).onConflict((conflict) => conflict.columns(["store_key", "job_id"]).doUpdateSet(values)));
	return normalized;
}
function deleteCronJobRowInDatabase(db, storeKey, jobId) {
	executeSqliteQuerySync(db, getCronStoreKysely(db).deleteFrom("cron_job_scratch").where("store_key", "=", storeKey).where("job_id", "=", jobId));
	executeSqliteQuerySync(db, getCronStoreKysely(db).deleteFrom("cron_jobs").where("store_key", "=", storeKey).where("job_id", "=", jobId));
}
/** Updates only mutable runtime columns without rewriting full job config JSON. */
function updateCronRuntimeRows(db, storeKey, store) {
	for (const job of store.jobs) executeSqliteQuerySync(db, getCronStoreKysely(db).updateTable("cron_jobs").set({
		state_json: serializeCronJobState(job.state ?? {}),
		runtime_updated_at_ms: job.updatedAtMs,
		schedule_identity: tryCronScheduleIdentity({ ...job })
	}).where("store_key", "=", storeKey).where("job_id", "=", job.id));
}
/** Reconstructs loaded cron store data and config-runtime sidecars from SQLite rows. */
function loadedCronStoreFromRows(rows) {
	const jobs = [];
	const configJobs = [];
	const configJobIndexes = [];
	const configJobRuntimeEntries = [];
	const invalidConfigRows = [];
	for (const [index, row] of rows.entries()) {
		const parsedJobJson = tryParseJsonObject(row.job_json);
		const parsedStateJson = tryParseJsonObject(row.state_json);
		if (!parsedJobJson || !parsedStateJson) {
			invalidConfigRows.push({
				sourceIndex: index,
				reason: parsedJobJson ? "invalid-state" : "invalid-payload",
				...parsedJobJson ? { job: decodeCronJobConfig(parsedJobJson) } : {},
				raw: {
					jobId: row.job_id,
					jobJson: row.job_json,
					stateJson: row.state_json
				}
			});
			continue;
		}
		const job = rowToCronJob(row, parsedJobJson);
		const configJob = decodeCronJobConfig(parsedJobJson);
		const runtimeEntry = {
			updatedAtMs: normalizeSqliteNumber(row.runtime_updated_at_ms) ?? normalizeSqliteNumber(row.updated_at),
			scheduleIdentity: row.schedule_identity ?? void 0,
			state: parsedStateJson
		};
		if (!job) {
			invalidConfigRows.push({
				sourceIndex: index,
				reason: getInvalidPersistedCronJobReason(configJob) ?? "invalid-payload",
				job: configJob,
				...runtimeEntry.state ? { state: runtimeEntry.state } : {},
				...runtimeEntry.updatedAtMs !== void 0 ? { updatedAtMs: runtimeEntry.updatedAtMs } : {},
				...runtimeEntry.scheduleIdentity !== void 0 ? { scheduleIdentity: runtimeEntry.scheduleIdentity } : {}
			});
			continue;
		}
		jobs.push(job);
		configJobs.push(configJob);
		configJobIndexes.push(index);
		configJobRuntimeEntries.push(runtimeEntry);
	}
	return {
		store: {
			version: 1,
			jobs
		},
		configJobs,
		configJobIndexes,
		configJobRuntimeEntries,
		invalidConfigRows
	};
}
//#endregion
export { loadedCronStoreFromRows as a, replaceCronRows as c, getCronStoreKysely as d, cronSchedulingInputsEqual as f, loadCronRows as i, updateCronRuntimeRows as l, normalizeCronJobIdentityFields as m, deleteCronJobRowInDatabase as n, materializeCronRowAgentOwners as o, tryCronScheduleIdentity as p, deleteStaleCronJobFamilyRows as r, projectCronJobThroughStorageCodec as s, assertCronStoreCanPersist as t, upsertCronJobRow as u };
