UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

372 lines (371 loc) 15.5 kB
import { i as getOrCreatePromise } from "./lazy-promise-DGqyc4Y4.js"; import { n as failDurableDelivery } from "./delivery-completion-BrAAYzVK.js"; import { o as createRenderedMessageBatchPlan, t as prepareOutboundPayloadBatch } from "./deliver-prepare-6uVAVf8W.js"; import { C as normalizeOutboundReplyFacts } from "./reply-payload-Bfsi665c.js"; import { m as resolveOutboundMediaAccessForSend, n as reconcileUnknownQueuedDelivery, s as collectPayloadMediaSources } from "./delivery-queue-reconciliation-kPHpqr5H.js"; import { _ as terminalizePendingDeliveryQueueEntry, i as countPendingDeliveryQueueEntries } from "./delivery-queue-sqlite-Bpn2g0Bx.js"; import { r as resolveOutboundMediaMaxBytes } from "./configured-max-bytes-CTSUbFDK.js"; import { A as movePendingDeliveryQueueEntryNamespace, F as createUnavailablePreparedOutboundBatch, I as mapPreparedOutboundAcceptedPayloads, P as acceptedPreparedOutboundEntries, R as projectPreparedOutboundBatchForStorage, d as loadLegacyPendingDeliveries, j as replacePendingDeliveryQueueEntry, m as loadPendingLegacyDeliveryPreparations, p as loadPendingDeliveryMigrations } from "./delivery-queue-storage-BMi71pp-.js"; import { a as OUTBOUND_DELIVERY_QUEUE_NAME, n as LEGACY_OUTBOUND_DELIVERY_QUEUE_NAME, o as OUTBOUND_LEGACY_PREPARATION_QUEUE_NAME, r as OUTBOUND_DELIVERY_MIGRATION_QUEUE_NAME, s as cancelDeliveryQueueMediaRetention, t as DELIVERY_QUEUE_MEDIA_STAGING_QUEUE_NAME } from "./delivery-queue-media-staging-DwGqhmAJ.js"; import { i as stageQueuePayloadMedia, r as releaseSpoolArtifacts, t as collectEntrySpoolPaths } from "./delivery-queue-media-spool-D2PN5qRE.js"; import { randomUUID } from "node:crypto"; //#region src/infra/outbound/delivery-queue-migration.ts const LEGACY_PREPARATION_LEASE_MS = 3e5; const LEGACY_PREPARATION_LEASE_RENEW_MS = 3e4; function withLegacyPreparationLease(entry, ownerId, now = Date.now()) { return { ...entry, retainOnFailure: true, legacyPreparationOwnerId: ownerId, legacyPreparationLeaseExpiresAt: now + LEGACY_PREPARATION_LEASE_MS }; } function hasActiveLegacyPreparationLease(entry, now = Date.now()) { return Boolean(entry.legacyPreparationOwnerId && typeof entry.legacyPreparationLeaseExpiresAt === "number" && entry.legacyPreparationLeaseExpiresAt > now); } function buildLegacyPreparationParams(entry, cfg) { const reply = normalizeOutboundReplyFacts({ reply: entry.reply, replyToId: entry.replyToId, replyToMode: entry.replyToMode }); return { cfg, channel: entry.channel, to: entry.to, accountId: entry.accountId, queuePolicy: entry.queuePolicy, requireUnknownSendReconciliation: entry.requireUnknownSendReconciliation, payloads: entry.payloads, renderedBatchPlan: entry.renderedBatchPlan, threadId: entry.threadId, reply, formatting: entry.formatting, identity: entry.identity, bestEffort: entry.bestEffort, gifPlayback: entry.gifPlayback, forceDocument: entry.forceDocument, replyPayloadSendingHook: entry.replyPayloadSendingHook, silent: entry.silent, mirror: entry.mirror, session: entry.session, gatewayClientScopes: entry.gatewayClientScopes, preparedMessageId: entry.preparedMessageId, deliveryCompletion: entry.deliveryCompletion, completionRetention: entry.completionRetention, skipQueue: true }; } async function prepareLegacyEntryCheckpoint(params) { const preparationParams = buildLegacyPreparationParams(params.entry, params.cfg); const reply = preparationParams.reply; const needsUnknownReconciliation = params.entry.recoveryState === "send_attempt_started" || params.entry.recoveryState === "unknown_after_send"; const legacyUnknownSendReconciliation = needsUnknownReconciliation ? await reconcileUnknownQueuedDelivery({ entry: { ...params.entry, reply }, payloads: params.entry.payloads, cfg: params.cfg, warn: (message) => params.log.warn(message) }) : void 0; if (needsUnknownReconciliation && (legacyUnknownSendReconciliation == null || legacyUnknownSendReconciliation.status === "unresolved")) { await failInterruptedLegacyPreparation({ entry: params.entry, log: params.log, stateDir: params.stateDir }); return "skipped"; } const prepareForReplay = !needsUnknownReconciliation || params.entry.recoveryState === "send_attempt_started" && legacyUnknownSendReconciliation?.status === "not_sent"; let sourceEntry = params.entry; let preparedBatch; if (prepareForReplay) { let modifiersStarted = false; let leaseLost = false; const renewLease = () => { if (leaseLost) return; const renewed = withLegacyPreparationLease(sourceEntry, params.ownerId); if (!replacePendingDeliveryQueueEntry({ queueName: "outbound-legacy-preparing-v1", expectedEntry: sourceEntry, replacementEntry: renewed, stateDir: params.stateDir })) { leaseLost = true; return; } sourceEntry = renewed; }; const renewLeaseSafely = () => { try { renewLease(); } catch (error) { leaseLost = true; params.log.warn(`Legacy delivery ${params.entry.id} preparation lease renewal failed: ${String(error)}`); } }; const leaseTimer = setInterval(renewLeaseSafely, LEGACY_PREPARATION_LEASE_RENEW_MS); leaseTimer.unref(); try { preparedBatch = await prepareOutboundPayloadBatch(preparationParams, { onBeforeFirstModifier: () => { if (leaseLost) throw new Error(`Legacy delivery ${params.entry.id} preparation lease was lost`); const startedEntry = { ...sourceEntry, legacyPreparationState: "modifiers_started" }; if (!replacePendingDeliveryQueueEntry({ queueName: "outbound-legacy-preparing-v1", expectedEntry: sourceEntry, replacementEntry: startedEntry, stateDir: params.stateDir })) throw new Error(`Legacy delivery ${params.entry.id} preparation ownership changed`); sourceEntry = startedEntry; modifiersStarted = true; } }); if (leaseLost) throw new Error(`Legacy delivery ${params.entry.id} preparation lease was lost`); } catch (error) { clearInterval(leaseTimer); if (modifiersStarted) await failInterruptedLegacyPreparation({ entry: sourceEntry, log: params.log, stateDir: params.stateDir }); else if (!leaseLost) { const releasedEntry = { ...sourceEntry, legacyPreparationOwnerId: void 0, legacyPreparationLeaseExpiresAt: void 0 }; replacePendingDeliveryQueueEntry({ queueName: OUTBOUND_LEGACY_PREPARATION_QUEUE_NAME, expectedEntry: sourceEntry, replacementEntry: releasedEntry, stateDir: params.stateDir }); } throw error; } clearInterval(leaseTimer); } else preparedBatch = createUnavailablePreparedOutboundBatch(params.entry.payloads.length); const acceptedPayloads = acceptedPreparedOutboundEntries(preparedBatch).map((entry) => entry.payload); const { payloads: _legacyPayloads, replyPayloadSendingHook: _legacyReplyHook, replyToId: _legacyReplyToId, replyToMode: _legacyReplyToMode, legacyPreparationState: _legacyPreparationState, legacyPreparationOwnerId: _legacyPreparationOwnerId, legacyPreparationLeaseExpiresAt: _legacyPreparationLeaseExpiresAt, ...retained } = params.entry; let canonicalRetained = retained; if (prepareForReplay && needsUnknownReconciliation) { const { availableAt: _availableAt, producerClaimId: _producerClaimId, platformSendAttemptId: _platformSendAttemptId, platformSendStartedAt: _platformSendStartedAt, effectiveReplyToId: _effectiveReplyToId, recoveryState: _recoveryState, ...resetAttempt } = retained; canonicalRetained = resetAttempt; } const checkpoint = { ...canonicalRetained, retainOnFailure: true, preparedBatch: projectPreparedOutboundBatchForStorage(preparedBatch), renderedBatchPlan: createRenderedMessageBatchPlan(acceptedPayloads), ...reply ? { reply } : {}, ...!prepareForReplay && (legacyUnknownSendReconciliation?.status === "sent" || legacyUnknownSendReconciliation?.status === "not_sent") ? { legacyUnknownSendReconciliation } : {}, ...!prepareForReplay ? { legacyPreparedContentUnavailable: true } : {} }; const result = movePendingDeliveryQueueEntryNamespace({ sourceQueueName: OUTBOUND_LEGACY_PREPARATION_QUEUE_NAME, destinationQueueName: OUTBOUND_DELIVERY_MIGRATION_QUEUE_NAME, expectedSourceEntry: sourceEntry, destinationEntry: checkpoint, stateDir: params.stateDir }); if (result !== "moved") { params.log.warn(`Legacy delivery ${params.entry.id} preparation deferred: ${result}`); return "skipped"; } return "checkpointed"; } async function failInterruptedLegacyPreparation(params) { if (terminalizePendingDeliveryQueueEntry({ queueName: "outbound-legacy-preparing-v1", id: params.entry.id, entry: params.entry, stateDir: params.stateDir }).status !== "terminalized") { params.log.warn(`Legacy delivery ${params.entry.id} preparation owner was already settled`); return; } if (params.entry.deliveryCompletion) try { await failDurableDelivery(params.entry.deliveryCompletion, params.stateDir); } catch (error) { params.log.warn(`Legacy delivery ${params.entry.id} interrupted preparation owner could not be marked unknown: ${String(error)}`); } await releaseSpoolArtifacts(collectEntrySpoolPaths(params.entry.payloads, params.stateDir), params.stateDir).catch((error) => { params.log.warn(`Legacy delivery ${params.entry.id} failed preparation media cleanup failed: ${String(error)}`); }); } function claimLegacyEntryForPreparation(params) { const claimed = withLegacyPreparationLease({ ...params.entry, legacyPreparationState: "claimed" }, params.ownerId); return movePendingDeliveryQueueEntryNamespace({ sourceQueueName: "outbound", destinationQueueName: "outbound-legacy-preparing-v1", expectedSourceEntry: params.entry, destinationEntry: claimed, retainSourceCompletionFence: params.entry.requiresProducerClaim === true || params.entry.completionRetention !== void 0, stateDir: params.stateDir }) === "moved" ? claimed : null; } function reclaimLegacyPreparation(params) { const claimed = withLegacyPreparationLease(params.entry, params.ownerId); return replacePendingDeliveryQueueEntry({ queueName: "outbound-legacy-preparing-v1", expectedEntry: params.entry, replacementEntry: claimed, stateDir: params.stateDir }) ? claimed : null; } async function finalizePreparedMigration(params) { const acceptedPayloads = acceptedPreparedOutboundEntries(params.entry.preparedBatch).map((entry) => entry.payload); const stageForReplay = params.entry.legacyPreparedContentUnavailable !== true; let stagedPayloads = acceptedPayloads; let mediaStageId; let stagedArtifacts = []; if (stageForReplay) { const mediaParams = { ...params.entry, cfg: params.cfg, payloads: acceptedPayloads, skipQueue: true }; const staged = await stageQueuePayloadMedia({ payloads: acceptedPayloads, mediaAccess: resolveOutboundMediaAccessForSend(mediaParams, params.entry.channel, collectPayloadMediaSources(acceptedPayloads)), maxBytes: resolveOutboundMediaMaxBytes({ cfg: params.cfg, channel: params.entry.channel, accountId: params.entry.accountId }), stateDir: params.stateDir }); if (staged.status !== "staged") { params.log.warn(`Legacy delivery ${params.entry.id} cannot be migrated: ${staged.reason} is not durable`); return "skipped"; } stagedPayloads = staged.payloads; mediaStageId = staged.mediaStageId; stagedArtifacts = staged.artifacts; } let stagedArtifactsTransferred = false; try { const queuedPreparedBatch = mapPreparedOutboundAcceptedPayloads(params.entry.preparedBatch, stagedPayloads); const destination = { ...params.entry, preparedBatch: queuedPreparedBatch, renderedBatchPlan: params.entry.renderedBatchPlan ?? createRenderedMessageBatchPlan(acceptedPayloads) }; const result = movePendingDeliveryQueueEntryNamespace({ sourceQueueName: OUTBOUND_DELIVERY_MIGRATION_QUEUE_NAME, destinationQueueName: OUTBOUND_DELIVERY_QUEUE_NAME, expectedSourceEntry: params.entry, destinationEntry: destination, ...mediaStageId ? { stagingQueueName: DELIVERY_QUEUE_MEDIA_STAGING_QUEUE_NAME, stagingId: mediaStageId } : {}, stateDir: params.stateDir }); if (result !== "moved") { params.log.warn(`Legacy delivery ${params.entry.id} migration deferred: ${result}`); return "skipped"; } stagedArtifactsTransferred = true; if (stageForReplay) await releaseSpoolArtifacts(collectEntrySpoolPaths(acceptedPayloads, params.stateDir), params.stateDir).catch((error) => { params.log.warn(`Legacy delivery ${params.entry.id} moved but old media cleanup failed: ${String(error)}`); }); return "moved"; } finally { if (!stagedArtifactsTransferred) { cancelDeliveryQueueMediaRetention(mediaStageId, params.stateDir); await releaseSpoolArtifacts(stagedArtifacts, params.stateDir); } } } const activeLegacyMigrations = /* @__PURE__ */ new Map(); /** Migrates every unchanged pre-D4 pending row before canonical recovery scans. */ async function migrateLegacyPendingOutboundDeliveries(params) { const migrationKey = params.stateDir ?? "<default-state>"; return await getOrCreatePromise(activeLegacyMigrations, migrationKey, () => migrateLegacyPendingOutboundDeliveriesOwned(params), { evictOnSettled: true }); } async function migrateLegacyPendingOutboundDeliveriesOwned(params) { let moved = 0; let skipped = 0; const ownerId = randomUUID(); const claimedPreparations = []; for (const entry of loadPendingLegacyDeliveryPreparations(params.stateDir)) { if (hasActiveLegacyPreparationLease(entry)) { skipped += 1; params.log.info(`Legacy delivery ${entry.id} preparation is leased by another owner`); continue; } if (entry.legacyPreparationState !== "claimed") { await failInterruptedLegacyPreparation({ ...params, entry }); skipped += 1; continue; } const claimed = reclaimLegacyPreparation({ entry, ownerId, stateDir: params.stateDir }); if (!claimed) { skipped += 1; params.log.info(`Legacy delivery ${entry.id} preparation ownership changed`); continue; } claimedPreparations.push(claimed); } for (const entry of loadLegacyPendingDeliveries(params.stateDir)) { const claimed = claimLegacyEntryForPreparation({ entry, ownerId, stateDir: params.stateDir }); if (!claimed) { skipped += 1; params.log.warn(`Legacy delivery ${entry.id} could not acquire preparation ownership`); continue; } claimedPreparations.push(claimed); } for (const entry of claimedPreparations) try { if (await prepareLegacyEntryCheckpoint({ ...params, entry, ownerId }) === "skipped") skipped += 1; } catch (error) { skipped += 1; params.log.warn(`Legacy delivery ${entry.id} migration failed: ${String(error)}`); } for (const entry of loadPendingDeliveryMigrations(params.stateDir)) try { if (await finalizePreparedMigration({ ...params, entry }) === "moved") moved += 1; else skipped += 1; } catch (error) { skipped += 1; params.log.warn(`Prepared delivery ${entry.id} migration failed: ${String(error)}`); } if (moved > 0 || skipped > 0) params.log.info(`Legacy delivery migration settled moved=${moved} skipped=${skipped}`); const remaining = countPendingDeliveryQueueEntries([ OUTBOUND_LEGACY_PREPARATION_QUEUE_NAME, LEGACY_OUTBOUND_DELIVERY_QUEUE_NAME, OUTBOUND_DELIVERY_MIGRATION_QUEUE_NAME ], params.stateDir); return { moved, skipped, remaining }; } //#endregion export { migrateLegacyPendingOutboundDeliveries };