UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

129 lines (128 loc) 6.33 kB
import { i as resolveGlobalSingleton } from "./global-singleton-Dc_stLtU.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { C as runWithRetainedGatewayRootWork } from "./gateway-work-admission-R1IpuDim.js"; import { n as captureAsyncWorkTracker, r as getAsyncWorkSignal, t as AsyncWorkScope } from "./async-work-scope-CMQS2uTf.js"; import { AsyncLocalStorage } from "node:async_hooks"; //#region src/infra/question-channel-runtime-internal.ts const TERMINAL_DELIVERY_RETENTION_MS = 864e5; function collectAnsweredLabels(record, event) { const answers = event.answers.answers; return record.questions.flatMap((question) => { if (question.isSecret || question.options.length === 0) return []; const optionLabels = new Set(question.options.map((option) => option.label)); return (answers[question.questionId] ?? []).filter((answer) => optionLabels.has(answer)); }); } function formatQuestionTerminalStatusLine(params) { if (params.event.status === "expired") return "Expired"; if (params.event.status === "cancelled") return "Cancelled"; const labels = collectAnsweredLabels(params.record, params.event); return labels.length > 0 ? `Answered: ${labels.join(", ")}` : "Answered"; } function createQuestionChannelRuntime(options = {}) { const entries = /* @__PURE__ */ new Map(); const retainedEntries = /* @__PURE__ */ new Set(); const deliveryContext = new AsyncLocalStorage(); const retiredGateways = /* @__PURE__ */ new WeakSet(); let finalizers = new AsyncWorkScope(); let clearing; const terminalRetentionMs = options.terminalRetentionMs ?? TERMINAL_DELIVERY_RETENTION_MS; const runFinalizer = (questionId, deliveryId, finalize, statusLine, track) => { finalizers.track(() => track(() => runWithRetainedGatewayRootWork(() => finalize(statusLine)))).catch((error) => options.onFinalizeError?.(error, questionId, deliveryId)); }; const finalizeDelivery = (questionId, entry, deliveryId, finalize) => { if (!entry.terminal || entry.finalizedDeliveryIds.has(deliveryId)) return; entry.deliveries.delete(deliveryId); entry.finalizedDeliveryIds.add(deliveryId); const statusLine = formatQuestionTerminalStatusLine({ record: entry.record, event: entry.terminal }); runFinalizer(questionId, deliveryId, finalize, statusLine, entry.track); }; const releaseEntry = (entry) => { retainedEntries.delete(entry); if (entries.get(entry.record.id) === entry) entries.delete(entry.record.id); clearTimeout(entry.cleanupTimer); entry.deliveries.clear(); entry.finalizedDeliveryIds.clear(); }; const scheduleCleanup = (entry) => { if (entry.cleanupTimer || !retainedEntries.has(entry)) return; entry.cleanupTimer = setTimeout(() => releaseEntry(entry), terminalRetentionMs); entry.cleanupTimer.unref?.(); }; return { handleRequested(record) { const owner = getAsyncWorkSignal(); if (clearing || owner && retiredGateways.has(owner)) return; const entry = { record, owner, track: captureAsyncWorkTracker(), deliveries: /* @__PURE__ */ new Map(), finalizedDeliveryIds: /* @__PURE__ */ new Set() }; retainedEntries.add(entry); entries.set(record.id, entry); }, handleResolved(event) { const entry = entries.get(event.id); if (!entry || entry.terminal) return; entry.terminal = event; for (const [deliveryId, finalize] of entry.deliveries) finalizeDelivery(event.id, entry, deliveryId, finalize); scheduleCleanup(entry); }, runWithDeliveries(questionIds, run, deliveryOptions) { if (!questionIds.some(Boolean)) return run(); const parent = deliveryContext.getStore(); const captured = new Map(parent?.entries); for (const id of questionIds) if (id && (deliveryOptions?.unbound || !captured.has(id))) captured.set(id, deliveryOptions?.unbound ? void 0 : entries.get(id)); return deliveryContext.run({ entries: captured, finalizers: parent?.finalizers ?? finalizers, owner: parent ? parent.owner : getAsyncWorkSignal(), track: parent ? parent.track : captureAsyncWorkTracker() }, run); }, registerDelivery({ questionId, deliveryId, finalize }) { const captured = deliveryContext.getStore(); if (clearing || captured && (captured.finalizers !== finalizers || captured.owner && retiredGateways.has(captured.owner))) return; const entry = captured?.entries.has(questionId) ? captured.entries.get(questionId) : entries.get(questionId); if (entry?.owner && retiredGateways.has(entry.owner)) return; if (!entry || !retainedEntries.has(entry)) { if (captured?.entries.has(questionId)) runFinalizer(questionId, deliveryId, finalize, "Unavailable: request a new question.", captured.track); return; } if (entry.finalizedDeliveryIds.has(deliveryId)) return; entry.deliveries.set(deliveryId, finalize); finalizeDelivery(questionId, entry, deliveryId, finalize); }, retireGateway(owner) { retiredGateways.add(owner); for (const entry of retainedEntries) if (entry.owner === owner) releaseEntry(entry); }, clear() { if (clearing) return clearing; for (const entry of retainedEntries) releaseEntry(entry); clearing = finalizers.drain().then(() => { finalizers = new AsyncWorkScope(); clearing = void 0; }); return clearing; } }; } //#endregion //#region src/infra/question-channel-runtime.ts const log = createSubsystemLogger("gateway/questions"); const questionChannelRuntime = resolveGlobalSingleton(Symbol.for("openclaw.questionChannelRuntime"), () => createQuestionChannelRuntime({ onFinalizeError: (error, questionId, deliveryId) => { log.warn(`question message finalization failed id=${questionId} delivery=${deliveryId}`, { error: String(error) }); } }), (runtime) => runtime.clear()); const handleQuestionChannelRequested = questionChannelRuntime.handleRequested; const handleQuestionChannelResolved = questionChannelRuntime.handleResolved; const runWithQuestionChannelDeliveries = questionChannelRuntime.runWithDeliveries; const registerQuestionChannelDelivery = questionChannelRuntime.registerDelivery; const retireQuestionChannelGateway = questionChannelRuntime.retireGateway; //#endregion export { runWithQuestionChannelDeliveries as a, retireQuestionChannelGateway as i, handleQuestionChannelResolved as n, registerQuestionChannelDelivery as r, handleQuestionChannelRequested as t };