UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

405 lines (404 loc) 19 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, f as normalizeStringifiedOptionalString } from "./string-coerce-mnp54Vah.js"; import { v as uniqueValues } from "./string-normalization-WNUDCpXX.js"; import { d as executeSqliteQuerySync, f as executeSqliteQueryTakeFirstSync, i as openOpenClawStateDatabase, o as runOpenClawStateWriteTransaction, p as getNodeSqliteKysely } from "./openclaw-state-db-ULtH_b7x.js"; import { t as parseByteSize } from "./parse-bytes-00heCtL_.js"; import { c as cronStoreKey } from "./normalize-Da7MTCo5.js"; import { l as resolveFailoverReasonFromError } from "./failover-error-DAwMDmz6.js"; import { i as normalizeCronRunDiagnostics } from "./run-diagnostics-DTgU7PXH.js"; //#region src/cron/run-log/entry-codec.ts /** Parses and normalizes persisted cron run-log entry payloads. */ const CRON_FAILOVER_REASONS = new Set([ "auth", "auth_permanent", "format", "rate_limit", "overloaded", "billing", "server_error", "timeout", "model_not_found", "session_expired", "empty_response", "no_error_details", "unclassified", "unknown" ]); function normalizeCronRunLogErrorReason(value) { return typeof value === "string" && CRON_FAILOVER_REASONS.has(value) ? value : void 0; } /** Parses a persisted cron run-log entry object and drops invalid or wrong-job rows. */ function parseCronRunLogEntryObject(obj, opts) { const jobId = normalizeOptionalString(opts?.jobId); if (!obj || typeof obj !== "object") return null; const entryObj = obj; if (entryObj.action !== "finished") return null; if (typeof entryObj.jobId !== "string" || entryObj.jobId.trim().length === 0) return null; if (typeof entryObj.ts !== "number" || !Number.isFinite(entryObj.ts)) return null; if (jobId && entryObj.jobId !== jobId) return null; const usage = entryObj.usage && typeof entryObj.usage === "object" ? entryObj.usage : void 0; const normalizedError = typeof entryObj.error === "string" ? entryObj.error : void 0; const normalizedProvider = typeof entryObj.provider === "string" && entryObj.provider.trim() ? entryObj.provider : void 0; const normalizedErrorReason = normalizeCronRunLogErrorReason(entryObj.errorReason) ?? resolveFailoverReasonFromError(normalizedError, normalizedProvider) ?? void 0; const entry = { ts: entryObj.ts, jobId: entryObj.jobId, action: "finished", status: entryObj.status, error: normalizedError, errorReason: normalizedErrorReason, summary: entryObj.summary, runId: typeof entryObj.runId === "string" && entryObj.runId.trim() ? entryObj.runId : void 0, diagnostics: normalizeCronRunDiagnostics(entryObj.diagnostics), runAtMs: entryObj.runAtMs, durationMs: entryObj.durationMs, nextRunAtMs: entryObj.nextRunAtMs, model: typeof entryObj.model === "string" && entryObj.model.trim() ? entryObj.model : void 0, provider: normalizedProvider, usage: usage ? { input_tokens: typeof usage.input_tokens === "number" ? usage.input_tokens : void 0, output_tokens: typeof usage.output_tokens === "number" ? usage.output_tokens : void 0, total_tokens: typeof usage.total_tokens === "number" ? usage.total_tokens : void 0, cache_read_tokens: typeof usage.cache_read_tokens === "number" ? usage.cache_read_tokens : void 0, cache_write_tokens: typeof usage.cache_write_tokens === "number" ? usage.cache_write_tokens : void 0 } : void 0 }; if (typeof entryObj.delivered === "boolean") entry.delivered = entryObj.delivered; if (entryObj.deliveryStatus === "delivered" || entryObj.deliveryStatus === "not-delivered" || entryObj.deliveryStatus === "unknown" || entryObj.deliveryStatus === "not-requested") entry.deliveryStatus = entryObj.deliveryStatus; if (typeof entryObj.deliveryError === "string") entry.deliveryError = entryObj.deliveryError; if (entryObj.failureNotificationDelivery && typeof entryObj.failureNotificationDelivery === "object") { const failureNotificationDelivery = entryObj.failureNotificationDelivery; if (failureNotificationDelivery.status === "delivered" || failureNotificationDelivery.status === "not-delivered" || failureNotificationDelivery.status === "unknown" || failureNotificationDelivery.status === "not-requested") entry.failureNotificationDelivery = { status: failureNotificationDelivery.status, ...typeof failureNotificationDelivery.delivered === "boolean" ? { delivered: failureNotificationDelivery.delivered } : {}, ...typeof failureNotificationDelivery.error === "string" ? { error: failureNotificationDelivery.error } : {} }; } if (entryObj.delivery && typeof entryObj.delivery === "object") entry.delivery = entryObj.delivery; if (typeof entryObj.sessionId === "string" && entryObj.sessionId.trim().length > 0) entry.sessionId = entryObj.sessionId; if (typeof entryObj.sessionKey === "string" && entryObj.sessionKey.trim().length > 0) entry.sessionKey = entryObj.sessionKey; return entry; } //#endregion //#region src/cron/run-log/sqlite-store.ts function getCronRunLogKysely(db) { return getNodeSqliteKysely(db); } function normalizeNumber(value) { if (typeof value === "bigint") return Number(value); return typeof value === "number" ? value : void 0; } function booleanToInteger(value) { return typeof value === "boolean" ? value ? 1 : 0 : null; } function integerToBoolean(value) { const normalized = normalizeNumber(value); return normalized == null ? void 0 : normalized !== 0; } function bindCronRunLogRow(params) { const entry = params.entry; return { store_key: params.storeKey, job_id: entry.jobId, seq: params.seq, ts: entry.ts, status: entry.status ?? null, error: entry.error ?? null, summary: entry.summary ?? null, diagnostics_summary: entry.diagnostics?.summary ?? null, delivery_status: entry.deliveryStatus ?? null, delivery_error: entry.deliveryError ?? null, delivered: booleanToInteger(entry.delivered), session_id: entry.sessionId ?? null, session_key: entry.sessionKey ?? null, run_id: entry.runId ?? null, run_at_ms: entry.runAtMs ?? null, duration_ms: entry.durationMs ?? null, next_run_at_ms: entry.nextRunAtMs ?? null, model: entry.model ?? null, provider: entry.provider ?? null, total_tokens: entry.usage?.total_tokens ?? null, entry_json: JSON.stringify(entry), created_at: Date.now() }; } /** Rehydrates a cron run-log row, preferring indexed SQLite columns over JSON payload values. */ function parseStoredRunLogEntry(row) { let rawEntry; try { rawEntry = JSON.parse(row.entry_json); } catch { return null; } const parsed = parseCronRunLogEntryObject(rawEntry, { jobId: row.job_id }); if (!parsed) return null; return { ...parsed, ts: normalizeNumber(row.ts) ?? parsed.ts, jobId: row.job_id, status: row.status ?? parsed.status, error: row.error ?? parsed.error, summary: row.summary ?? parsed.summary, delivered: integerToBoolean(row.delivered) ?? parsed.delivered, deliveryStatus: row.delivery_status ?? parsed.deliveryStatus, deliveryError: row.delivery_error ?? parsed.deliveryError, sessionId: row.session_id ?? parsed.sessionId, sessionKey: row.session_key ?? parsed.sessionKey, runId: row.run_id ?? parsed.runId, runAtMs: normalizeNumber(row.run_at_ms) ?? parsed.runAtMs, durationMs: normalizeNumber(row.duration_ms) ?? parsed.durationMs, nextRunAtMs: normalizeNumber(row.next_run_at_ms) ?? parsed.nextRunAtMs, model: row.model ?? parsed.model, provider: row.provider ?? parsed.provider }; } /** Reads run-log rows for one store, optionally scoped to one job, in chronological order. */ function readCronRunLogRows(db, storeKey, jobId) { let query = getCronRunLogKysely(db).selectFrom("cron_run_logs").selectAll().where("store_key", "=", storeKey); if (jobId) query = query.where("job_id", "=", jobId); return executeSqliteQuerySync(db, query.orderBy("ts", "asc").orderBy("seq", "asc")).rows; } function applyRunLogFilters(query, params) { let next = query.where("store_key", "=", params.storeKey); if (params.jobId) next = next.where("job_id", "=", params.jobId); if (params.statuses?.length) next = next.where("status", "in", params.statuses); if (params.deliveryStatuses?.length) next = next.where((eb) => eb.or(params.deliveryStatuses.map((status) => status === "not-requested" ? eb.or([eb("delivery_status", "is", null), eb("delivery_status", "=", status)]) : eb("delivery_status", "=", status)))); const runId = params.runId?.trim(); if (runId) next = next.where("run_id", "=", runId); return next; } /** Counts run-log rows after applying the same filters used by paged reads. */ function countCronRunLogRows(params) { return normalizeNumber(executeSqliteQueryTakeFirstSync(params.db, applyRunLogFilters(getCronRunLogKysely(params.db).selectFrom("cron_run_logs").select((eb) => eb.fn.countAll().as("count")), params))?.count ?? null) ?? 0; } /** Reads a sorted, filtered page of cron run-log rows. */ function readCronRunLogRowsPage(params) { let query = applyRunLogFilters(getCronRunLogKysely(params.db).selectFrom("cron_run_logs").selectAll(), params).orderBy("ts", params.sortDir).orderBy("seq", params.sortDir); if (params.limit !== void 0 && params.offset !== void 0) query = query.limit(params.limit).offset(params.offset); return executeSqliteQuerySync(params.db, query).rows; } function nextCronRunLogSeq(db, storeKey, jobId) { return (normalizeNumber(executeSqliteQueryTakeFirstSync(db, getCronRunLogKysely(db).selectFrom("cron_run_logs").select((eb) => eb.fn.max("seq").as("seq")).where("store_key", "=", storeKey).where("job_id", "=", jobId))?.seq ?? null) ?? 0) + 1; } /** Appends a cron run-log entry with a per-job monotonic sequence number. */ function insertCronRunLogEntry(db, storeKey, entry) { const seq = nextCronRunLogSeq(db, storeKey, entry.jobId); executeSqliteQuerySync(db, getCronRunLogKysely(db).insertInto("cron_run_logs").values(bindCronRunLogRow({ storeKey, seq, entry }))); } /** Prunes old cron run-log rows for one job, retaining the newest keepLines rows. */ function pruneCronRunLogRows(db, storeKey, jobId, keepLines) { const keep = Math.max(1, Math.floor(keepLines)); const keepSeqs = getCronRunLogKysely(db).selectFrom("cron_run_logs").select("seq").where("store_key", "=", storeKey).where("job_id", "=", jobId).orderBy("seq", "desc").limit(keep); executeSqliteQuerySync(db, getCronRunLogKysely(db).deleteFrom("cron_run_logs").where("store_key", "=", storeKey).where("job_id", "=", jobId).where("seq", "not in", keepSeqs)); } //#endregion //#region src/cron/run-log.ts /** Public cron run-log API with serialized writes and paged reads. */ const INVALID_CRON_RUN_LOG_JOB_ID_MESSAGE = "invalid cron run log job id"; function assertSafeCronRunLogJobId(jobId) { const trimmed = jobId.trim(); if (!trimmed) throw new Error(INVALID_CRON_RUN_LOG_JOB_ID_MESSAGE); if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\0")) throw new Error(INVALID_CRON_RUN_LOG_JOB_ID_MESSAGE); return trimmed; } /** Returns whether an error came from cron run-log job id validation. */ function isInvalidCronRunLogJobIdError(err) { return err instanceof Error && err.message === INVALID_CRON_RUN_LOG_JOB_ID_MESSAGE; } const writesByTarget = /* @__PURE__ */ new Map(); /** Legacy byte cap kept for config parsing compatibility with older file-backed run logs. */ const DEFAULT_CRON_RUN_LOG_MAX_BYTES = 2e6; /** Default SQLite row retention per cron job when no explicit keepLines value is configured. */ const DEFAULT_CRON_RUN_LOG_KEEP_LINES = 2e3; /** Resolves configured run-log pruning limits while preserving legacy maxBytes parsing. */ function resolveCronRunLogPruneOptions(cfg) { let maxBytes = DEFAULT_CRON_RUN_LOG_MAX_BYTES; if (cfg?.maxBytes !== void 0) try { const configuredMaxBytes = normalizeStringifiedOptionalString(cfg.maxBytes); if (configuredMaxBytes) maxBytes = parseByteSize(configuredMaxBytes, { defaultUnit: "b" }); } catch { maxBytes = DEFAULT_CRON_RUN_LOG_MAX_BYTES; } let keepLines = DEFAULT_CRON_RUN_LOG_KEEP_LINES; if (typeof cfg?.keepLines === "number" && Number.isFinite(cfg.keepLines) && cfg.keepLines > 0) keepLines = Math.floor(cfg.keepLines); return { maxBytes, keepLines }; } function cronRunLogWriteKey(storePath, jobId) { return `${cronStoreKey(storePath)}\0${jobId ?? ""}`; } async function drainPendingWrite(storePath, jobId) { if (jobId) { await writesByTarget.get(cronRunLogWriteKey(storePath, jobId))?.catch(() => void 0); return; } const storePrefix = `${cronStoreKey(storePath)}\0`; const pending = [...writesByTarget.entries()].filter(([key]) => key.startsWith(storePrefix)).map(([, write]) => write.catch(() => void 0)); await Promise.all(pending); } /** Appends a cron run-log row and serializes writes per store/job before pruning old rows. */ async function appendCronRunLog(params) { const storeKey = cronStoreKey(params.storePath); const writeKey = cronRunLogWriteKey(params.storePath, params.entry.jobId); const next = (writesByTarget.get(writeKey) ?? Promise.resolve()).catch(() => void 0).then(async () => { runOpenClawStateWriteTransaction(({ db }) => { insertCronRunLogEntry(db, storeKey, params.entry); if (params.opts?.keepLines !== false) pruneCronRunLogRows(db, storeKey, params.entry.jobId, params.opts?.keepLines ?? 2e3); }); }); writesByTarget.set(writeKey, next); try { await next; } finally { if (writesByTarget.get(writeKey) === next) writesByTarget.delete(writeKey); } } /** Reads recent run-log entries synchronously for startup/task reconciliation paths. */ function readCronRunLogEntriesSync(params) { const limit = Math.max(1, Math.min(5e3, Math.floor(params.limit ?? 200))); const storeKey = cronStoreKey(params.storePath); const jobId = params.jobId ? assertSafeCronRunLogJobId(params.jobId) : void 0; return readCronRunLogRows(openOpenClawStateDatabase().db, storeKey, jobId).map(parseStoredRunLogEntry).filter((entry) => entry !== null).slice(-limit); } function normalizeRunStatusFilter(status) { if (status === "ok" || status === "error" || status === "skipped" || status === "all") return status; return "all"; } function normalizeRunStatuses(opts) { if (Array.isArray(opts?.statuses) && opts.statuses.length > 0) { const filtered = opts.statuses.filter((status) => status === "ok" || status === "error" || status === "skipped"); if (filtered.length > 0) return uniqueValues(filtered); } const status = normalizeRunStatusFilter(opts?.status); if (status === "all") return null; return [status]; } function normalizeDeliveryStatuses(opts) { if (Array.isArray(opts?.deliveryStatuses) && opts.deliveryStatuses.length > 0) { const filtered = opts.deliveryStatuses.filter((status) => status === "delivered" || status === "not-delivered" || status === "unknown" || status === "not-requested"); if (filtered.length > 0) return uniqueValues(filtered); } if (opts?.deliveryStatus === "delivered" || opts?.deliveryStatus === "not-delivered" || opts?.deliveryStatus === "unknown" || opts?.deliveryStatus === "not-requested") return [opts.deliveryStatus]; return null; } function runIdMatches(entry, runId) { const normalized = normalizeOptionalString(runId); return !normalized || entry.runId === normalized; } function filterRunLogEntries(entries, opts) { return entries.filter((entry) => { if (!runIdMatches(entry, opts.runId)) return false; if (opts.statuses && (!entry.status || !opts.statuses.includes(entry.status))) return false; if (opts.deliveryStatuses) { const deliveryStatus = entry.deliveryStatus ?? "not-requested"; if (!opts.deliveryStatuses.includes(deliveryStatus)) return false; } if (!opts.query) return true; return normalizeLowercaseStringOrEmpty(opts.queryTextForEntry(entry)).includes(opts.query); }); } /** Reads a bounded, filterable run-log page for CLI and UI list views. */ async function readCronRunLogEntriesPage(opts) { const jobId = opts.jobId ? assertSafeCronRunLogJobId(opts.jobId) : void 0; await drainPendingWrite(opts.storePath, jobId); const limit = Math.max(1, Math.min(200, Math.floor(opts.limit ?? 50))); const statuses = normalizeRunStatuses(opts); const deliveryStatuses = normalizeDeliveryStatuses(opts); const query = normalizeLowercaseStringOrEmpty(opts.query); const sortDir = opts.sortDir === "asc" ? "asc" : "desc"; const db = openOpenClawStateDatabase().db; const storeKey = cronStoreKey(opts.storePath); const offset = Math.max(0, Math.floor(opts.offset ?? 0)); if (!query) { const total = countCronRunLogRows({ db, storeKey, jobId, statuses, deliveryStatuses, runId: opts.runId }); const boundedOffset = Math.min(total, offset); const entries = readCronRunLogRowsPage({ db, storeKey, jobId, statuses, deliveryStatuses, runId: opts.runId, sortDir, offset: boundedOffset, limit }).map(parseStoredRunLogEntry).filter((entry) => entry !== null); if (opts.jobNameById) for (const entry of entries) { const jobName = opts.jobNameById[entry.jobId]; if (jobName) entry.jobName = jobName; } const nextOffset = boundedOffset + entries.length; return { entries, total, offset: boundedOffset, limit, hasMore: nextOffset < total, nextOffset: nextOffset < total ? nextOffset : null }; } const filtered = filterRunLogEntries(readCronRunLogRowsPage({ db, storeKey, jobId, statuses, deliveryStatuses, runId: opts.runId, sortDir }).map(parseStoredRunLogEntry).filter((entry) => entry !== null), { runId: opts.runId, statuses: null, deliveryStatuses: null, query, queryTextForEntry: (entry) => { const jobName = opts.jobNameById?.[entry.jobId] ?? ""; return [ entry.summary ?? "", entry.error ?? "", entry.errorReason ?? "", entry.diagnostics?.summary ?? "", ...(entry.diagnostics?.entries ?? []).map((diagnostic) => diagnostic.message), entry.jobId, jobName, entry.delivery?.intended?.channel ?? "", entry.delivery?.resolved?.channel ?? "", ...(entry.delivery?.messageToolSentTo ?? []).map((target) => target.channel) ].join(" "); } }); const sorted = sortDir === "asc" ? filtered.toSorted((a, b) => a.ts - b.ts) : filtered.toSorted((a, b) => b.ts - a.ts); const total = sorted.length; const boundedOffset = Math.min(total, offset); const entries = sorted.slice(boundedOffset, boundedOffset + limit); if (opts.jobNameById) for (const entry of entries) { const jobName = opts.jobNameById[entry.jobId]; if (jobName) entry.jobName = jobName; } const nextOffset = boundedOffset + entries.length; return { entries, total, offset: boundedOffset, limit, hasMore: nextOffset < total, nextOffset: nextOffset < total ? nextOffset : null }; } /** Reads a run-log page across all jobs for a specific cron store. */ async function readCronRunLogEntriesPageAll(opts) { return readCronRunLogEntriesPage(opts); } //#endregion export { readCronRunLogEntriesSync as a, readCronRunLogEntriesPageAll as i, isInvalidCronRunLogJobIdError as n, resolveCronRunLogPruneOptions as o, readCronRunLogEntriesPage as r, parseCronRunLogEntryObject as s, appendCronRunLog as t };