UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

243 lines (242 loc) 13.5 kB
import "./src-vebZIeLe.js"; import { n as safeParseJsonRecord } from "./json-coercion-AulM0PZ6.js"; import { u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js"; import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { B as loadDeliveryQueueEntryInDatabase, F as projectDeliveryQueueTerminalEntry, H as pruneDeliveryQueueTombstones, I as bindDeliveryQueueEntry, L as deliveryQueueEntriesQuery, M as hasLiveDeliveryQueueClaim, N as inferDeliveryQueueFailureRetention, P as parseDeliveryQueueCompletionRetention, U as terminalizeBoundDeliveryQueueEntry, V as pruneDeliveryQueueTombstoneAges, W as upsertBoundDeliveryQueueEntryInDatabase, i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction, z as inflateDeliveryQueueRow } from "./openclaw-state-db-BRTnL-D8.js"; //#region src/infra/delivery-queue-sqlite.ts function openStateDatabase(stateDir) { return openOpenClawStateDatabase({ env: stateDir ? { ...process.env, OPENCLAW_STATE_DIR: stateDir } : process.env }); } function enoent(queueName, id) { const err = /* @__PURE__ */ new Error(`No pending ${queueName} delivery queue entry ${id}`); err.code = "ENOENT"; return err; } function upsertDeliveryQueueEntryInDatabase(params, database) { return upsertBoundDeliveryQueueEntryInDatabase(bindDeliveryQueueEntry(params), database); } /** Insert or replace a delivery queue entry under a queue namespace. */ function upsertDeliveryQueueEntry(params) { return upsertDeliveryQueueEntryInDatabase(params, openStateDatabase(params.stateDir)); } /** * Expire abandoned staging rows and capture destination/staging ownership in * one write snapshot. A concurrent commit either lands before this snapshot or * loses its staging row and must fail closed. */ function expireStagingAndLoadDeliveryQueueEntries(params) { const database = openStateDatabase(params.stateDir); const snapshot = runSqliteImmediateTransactionSync(database.db, () => { executeSqliteQuerySync(database.db, getNodeSqliteKysely(database.db).deleteFrom("delivery_queue_entries").where("queue_name", "=", params.stagingQueueName).where("status", "=", "pending").where("enqueued_at", "<=", params.expireBeforeMs)); const read = (queueNames) => executeSqliteQuerySync(database.db, deliveryQueueEntriesQuery(database, queueNames, "unfinished").orderBy("enqueued_at", "asc").orderBy("id", "asc")).rows; return { entryRows: read(params.queueNames), stagingRows: read([params.stagingQueueName]) }; }, { databaseLabel: "openclaw-state", operationLabel: "expire delivery queue staging entries" }); return { entries: snapshot.entryRows.map(inflateDeliveryQueueRow).filter((entry) => entry != null), stagingEntries: snapshot.stagingRows.map(inflateDeliveryQueueRow).filter((entry) => entry != null) }; } /** Load a single pending delivery queue entry. */ function loadDeliveryQueueEntry(queueName, id, stateDir, mode = "pending") { return loadDeliveryQueueEntryInDatabase(openStateDatabase(stateDir), queueName, id, mode); } /** Read row status without hiding dead-lettered entries. */ function getDeliveryQueueEntryStatus(queueName, id, stateDir) { return getDeliveryQueueEntryOwners([queueName], id, stateDir).get(queueName)?.status; } /** Read one exact ID across physical namespaces from a single ownership snapshot. */ function getDeliveryQueueEntryOwners(queueNames, id, stateDir) { if (queueNames.length === 0) return /* @__PURE__ */ new Map(); return getDeliveryQueueEntryOwnersInDatabase(openStateDatabase(stateDir), queueNames, id); } /** Keeps namespace reads and receipt pruning on the caller's exact transaction handle. */ function getDeliveryQueueEntryOwnersInDatabase(database, queueNames, id) { if (queueNames.length === 0) return /* @__PURE__ */ new Map(); const queueDb = getNodeSqliteKysely(database.db); return runSqliteImmediateTransactionSync(database.db, () => { const readExact = () => executeSqliteQuerySync(database.db, queueDb.selectFrom("delivery_queue_entries").select([ "queue_name", "status", "recovery_state" ]).select((eb) => eb.case("recovery_state").when("completed_bounded").then(eb.ref("entry_json")).else(null).end().as("entry_json")).where("queue_name", "in", queueNames).where("id", "=", id)).rows; let rows = readExact(); let pruned = false; for (const row of rows) { if (row.entry_json === null) continue; const entry = safeParseJsonRecord(row.entry_json); const retention = parseDeliveryQueueCompletionRetention(entry?.completionRetention, id); if (typeof retention === "object") { pruneDeliveryQueueTombstones(database.db, Date.now(), { queueName: row.queue_name, idPrefix: retention.idPrefix }); pruned = true; } } if (pruned) rows = readExact(); return new Map(rows.flatMap((row) => row.status ? [[row.queue_name, { status: row.status, ...row.status === "failed" && row.recovery_state === "settlement_pending" ? { settlementPending: true } : {} }]] : [])); }, { databaseLabel: "openclaw-state", operationLabel: "read delivery queue status" }); } /** Load all pending entries for a queue namespace in database order. */ function loadDeliveryQueueEntries(queueName, stateDir, mode = "pending") { const database = openStateDatabase(stateDir); return executeSqliteQuerySync(database.db, deliveryQueueEntriesQuery(database, [queueName], mode).orderBy("enqueued_at", "asc").orderBy("id", "asc")).rows.map(inflateDeliveryQueueRow).filter((entry) => entry != null); } /** Delete a pending delivery queue entry after successful delivery. */ function deleteDeliveryQueueEntry(queueName, id, stateDir) { deleteDeliveryQueueEntryInDatabase(openStateDatabase(stateDir), queueName, id); } function deleteDeliveryQueueEntryInDatabase(database, queueName, id) { const queueDb = getNodeSqliteKysely(database.db); executeSqliteQuerySync(database.db, queueDb.deleteFrom("delivery_queue_entries").where("queue_name", "=", queueName).where("id", "=", id).where("status", "=", "pending")); } /** Retain a delivered row as a durable idempotency tombstone. */ function completeDeliveryQueueEntry(queueName, id, stateDir) { completeDeliveryQueueEntryInDatabase(openStateDatabase(stateDir), queueName, id); } function completeDeliveryQueueEntryInDatabase(database, queueName, id) { const now = Date.now(); const requestedRetention = loadDeliveryQueueEntryInDatabase(database, queueName, id, "pending")?.completionRetention; const retention = parseDeliveryQueueCompletionRetention(requestedRetention, id); if (requestedRetention && !retention) throw new Error(`Invalid bounded delivery completion retention: ${queueName}/${id}`); if (!upsertDeliveryQueueEntryInDatabase({ queueName, entry: projectDeliveryQueueTerminalEntry({ id, retryCount: 0 }, now, "completed", retention), metadata: {}, status: "completed", completeExisting: true }, database)) { if (getDeliveryQueueEntryOwnersInDatabase(database, [queueName], id).get(queueName)?.status === "completed") return; throw enoent(queueName, id); } if (typeof retention === "object") getDeliveryQueueEntryOwnersInDatabase(database, [queueName], id); } /** Load, transform, and persist a pending delivery queue entry. */ function updateDeliveryQueueEntry(queueName, id, stateDir, update) { const database = openStateDatabase(stateDir); const current = loadDeliveryQueueEntryInDatabase(database, queueName, id, "pending"); if (!current) throw enoent(queueName, id); upsertDeliveryQueueEntryInDatabase({ queueName, entry: update(current) }, database); } /** Atomically reserve one provider-delivery call before executing it. */ function reserveDeliveryQueueEntryAttempt(params) { if (!Number.isInteger(params.maxAttempts) || params.maxAttempts <= 0) throw new Error(`Invalid delivery attempt budget: ${params.maxAttempts}`); return runOpenClawStateWriteTransaction((database) => { const current = loadDeliveryQueueEntryInDatabase(database, params.queueName, params.id, "pending"); if (!current) throw enoent(params.queueName, params.id); if (params.expectedPlatformSendAttemptId && !hasLiveDeliveryQueueClaim(current, params.expectedPlatformSendAttemptId, Date.now())) throw new Error(`Delivery platform claim was lost: ${params.id}`); const persistedAttemptCount = typeof current.attemptCount === "number" && Number.isInteger(current.attemptCount) && current.attemptCount >= 0 ? current.attemptCount : 0; const attemptCount = Math.max(persistedAttemptCount, current.retryCount); if (attemptCount >= params.maxAttempts) return { status: "exhausted", attemptCount }; const reservedAttemptCount = attemptCount + 1; if (!upsertDeliveryQueueEntryInDatabase({ queueName: params.queueName, entry: { ...current, attemptCount: reservedAttemptCount }, updatePendingOnly: true }, database)) throw enoent(params.queueName, params.id); return { status: "reserved", attemptCount: reservedAttemptCount }; }, { env: params.stateDir ? { ...process.env, OPENCLAW_STATE_DIR: params.stateDir } : process.env }, { operationLabel: `reserve ${params.queueName} delivery attempt` }); } /** Count dead-lettered entries per queue namespace for coarse health reporting. */ function countFailedDeliveryQueueEntries(stateDir) { const database = openStateDatabase(stateDir); const queueDb = getNodeSqliteKysely(database.db); return executeSqliteQuerySync(database.db, queueDb.selectFrom("delivery_queue_entries").select((eb) => [ "queue_name as queueName", eb.fn.countAll().as("count"), eb.fn.min("failed_at").as("oldestFailedAt") ]).where("status", "=", "failed").groupBy("queue_name").orderBy("queue_name", "asc")).rows.map(({ oldestFailedAt, ...row }) => oldestFailedAt == null ? row : Object.assign(row, { oldestFailedAt })); } /** Count pending entries across an exact set of queue namespaces. */ function countPendingDeliveryQueueEntries(queueNames, stateDir) { if (queueNames.length === 0) return 0; const database = openStateDatabase(stateDir); const queueDb = getNodeSqliteKysely(database.db); const [row] = executeSqliteQuerySync(database.db, queueDb.selectFrom("delivery_queue_entries").select((eb) => eb.fn.countAll().as("count")).where("queue_name", "in", queueNames).where("status", "=", "pending")).rows; return row?.count ?? 0; } /** Physically expire age-bounded delivery queue tombstones. */ function pruneExpiredDeliveryQueueTombstones(stateDir) { const database = openStateDatabase(stateDir); runSqliteImmediateTransactionSync(database.db, () => pruneDeliveryQueueTombstoneAges(database.db, Date.now()), { databaseLabel: "openclaw-state", operationLabel: "expire delivery queue tombstones" }); } /** Terminalize one pending row using its failure-retention ownership fact. */ function moveDeliveryQueueEntryToFailed(queueName, id, stateDir) { const current = loadDeliveryQueueEntry(queueName, id, stateDir); if (!current) throw enoent(queueName, id); if (terminalizePendingDeliveryQueueEntry({ queueName, id, entry: current, stateDir }).status !== "terminalized") throw enoent(queueName, id); } /** Validate and serialize terminal custody before a standalone call opens its database. */ function prepareDeliveryQueueTerminalEntry(params) { if (params.entry.id !== params.id) throw new Error(`Delivery queue entry id mismatch: ${params.entry.id} != ${params.id}`); const now = Date.now(); const expectedJson = JSON.stringify(params.entry); const retention = inferDeliveryQueueFailureRetention(params.entry, params.id, params.queueName); const failedEntry = retention ? projectDeliveryQueueTerminalEntry(params.entry, now, "failed", retention) : void 0; return { queueName: params.queueName, id: params.id, expectedStatus: params.expectedStatus, now, expectedJson, retention, failedEntry }; } /** Atomically delete or tombstone a pending row only while its value is unchanged. */ function terminalizePendingDeliveryQueueEntry(params) { const prepared = prepareDeliveryQueueTerminalEntry(params); return terminalizePendingDeliveryQueueEntryInDatabase(openStateDatabase(params.stateDir), prepared); } function terminalizePendingDeliveryQueueEntryInDatabase(database, prepared) { const { queueName, id, expectedJson, failedEntry, now, expectedStatus, retention } = prepared; if (!terminalizeBoundDeliveryQueueEntry(database.db, queueName, id, expectedJson, failedEntry, now, expectedStatus)) return { status: "not_pending" }; if (typeof retention === "object") getDeliveryQueueEntryOwnersInDatabase(database, [queueName], id); return { status: "terminalized", retained: retention !== void 0 }; } //#endregion export { terminalizePendingDeliveryQueueEntry as _, deleteDeliveryQueueEntry as a, upsertDeliveryQueueEntry as b, getDeliveryQueueEntryOwners as c, loadDeliveryQueueEntries as d, loadDeliveryQueueEntry as f, reserveDeliveryQueueEntryAttempt as g, pruneExpiredDeliveryQueueTombstones as h, countPendingDeliveryQueueEntries as i, getDeliveryQueueEntryOwnersInDatabase as l, prepareDeliveryQueueTerminalEntry as m, completeDeliveryQueueEntryInDatabase as n, deleteDeliveryQueueEntryInDatabase as o, moveDeliveryQueueEntryToFailed as p, countFailedDeliveryQueueEntries as r, expireStagingAndLoadDeliveryQueueEntries as s, completeDeliveryQueueEntry as t, getDeliveryQueueEntryStatus as u, terminalizePendingDeliveryQueueEntryInDatabase as v, upsertDeliveryQueueEntryInDatabase as x, updateDeliveryQueueEntry as y };