UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

955 lines (954 loc) 36.4 kB
import { c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js"; import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js"; import "./parse-finite-number-Z7n6tXLk.js"; import { t as resolveGlobalMap } from "./global-singleton-PwlQSEal.js"; import { n as defaultRuntime } from "./runtime-B4lgFmsS.js"; import { t as parseDurationMs } from "./parse-duration-oxu_S07c.js"; import { n as resolveGlobalDedupeCache } from "./dedupe-DnzL4okR.js"; import { n as channelRouteDedupeKey, t as channelRouteCompactKey } from "./channel-route-D7X5briz.js"; import { n as getLoadedChannelPlugin } from "./registry-MkxNn1Ue.js"; import "./plugins-t2ejWcVy.js"; import { r as clearCommandLane } from "./command-queue-DGvXlE4p.js"; import { t as resolveEmbeddedSessionLane } from "./lanes-CVttd5qX.js"; import { t as isRoutableChannel } from "./route-reply-Br98TMTR.js"; //#region src/auto-reply/reply/directive-parsing.ts /** Low-level token scanning helpers for inline directive parsers. */ function skipDirectiveArgPrefix(raw) { let i = 0; const len = raw.length; while (i < len && /\s/.test(raw[i])) i += 1; if (raw[i] === ":") { i += 1; while (i < len && /\s/.test(raw[i])) i += 1; } return i; } /** Reads the next non-whitespace directive token and returns the next scan index. */ function takeDirectiveToken(raw, startIndex) { let i = startIndex; const len = raw.length; while (i < len && /\s/.test(raw[i])) i += 1; if (i >= len) return { token: null, nextIndex: i }; const start = i; while (i < len && !/\s/.test(raw[i])) i += 1; if (start === i) return { token: null, nextIndex: i }; const token = raw.slice(start, i); while (i < len && /\s/.test(raw[i])) i += 1; return { token, nextIndex: i }; } //#endregion //#region src/auto-reply/reply/queue/normalize.ts /** Normalizes user-entered queue mode aliases from directives/config. */ function normalizeQueueMode(raw) { const cleaned = normalizeOptionalLowercaseString(raw); if (!cleaned) return; if (cleaned === "interrupt" || cleaned === "interrupts" || cleaned === "abort") return "interrupt"; if (cleaned === "steer" || cleaned === "steering") return "steer"; if (cleaned === "followup" || cleaned === "follow-ups" || cleaned === "followups") return "followup"; if (cleaned === "collect" || cleaned === "coalesce") return "collect"; } /** Normalizes persisted legacy queue mode aliases into current queue modes. */ function normalizePersistedQueueMode(raw) { const normalized = normalizeQueueMode(raw); if (normalized) return normalized; const cleaned = normalizeOptionalLowercaseString(raw); if (cleaned === "queue" || cleaned === "queued") return "steer"; if (cleaned === "steer+backlog" || cleaned === "steer-backlog" || cleaned === "steer_backlog") return "followup"; } /** Normalizes queue drop policy aliases from directives/config. */ function normalizeQueueDropPolicy(raw) { const cleaned = normalizeOptionalLowercaseString(raw); if (!cleaned) return; if (cleaned === "old" || cleaned === "oldest") return "old"; if (cleaned === "new" || cleaned === "newest") return "new"; if (cleaned === "summarize" || cleaned === "summary") return "summarize"; } //#endregion //#region src/auto-reply/reply/queue/directive.ts /** Parses debounce durations in `/queue` directives. */ function parseQueueDebounce(raw) { if (!raw) return; try { const parsed = parseDurationMs(raw.trim(), { defaultUnit: "ms" }); if (!parsed || parsed < 0) return; return Math.round(parsed); } catch { return; } } function parseQueueCap(raw) { if (!raw) return; return parseStrictPositiveInteger(raw); } function parseQueueDirectiveArgs(raw) { const len = raw.length; let i = skipDirectiveArgPrefix(raw); let consumed = i; let queueMode; let queueReset = false; let rawMode; let debounceMs; let cap; let dropPolicy; let rawDebounce; let rawCap; let rawDrop; let hasOptions = false; const takeToken = () => { const res = takeDirectiveToken(raw, i); i = res.nextIndex; return res.token; }; for (;;) { if (i >= len) break; const token = takeToken(); if (!token) break; const lowered = normalizeOptionalLowercaseString(token); if (!lowered) break; if (lowered === "default" || lowered === "reset" || lowered === "clear") { queueReset = true; consumed = i; break; } if (lowered.startsWith("debounce:") || lowered.startsWith("debounce=")) { rawDebounce = token.split(/[:=]/)[1] ?? ""; debounceMs = parseQueueDebounce(rawDebounce); hasOptions = true; consumed = i; continue; } if (lowered.startsWith("cap:") || lowered.startsWith("cap=")) { rawCap = token.split(/[:=]/)[1] ?? ""; cap = parseQueueCap(rawCap); hasOptions = true; consumed = i; continue; } if (lowered.startsWith("drop:") || lowered.startsWith("drop=")) { rawDrop = token.split(/[:=]/)[1] ?? ""; dropPolicy = normalizeQueueDropPolicy(rawDrop); hasOptions = true; consumed = i; continue; } const mode = normalizeQueueMode(token); if (mode) { queueMode = mode; rawMode = token; consumed = i; continue; } if (consumed === skipDirectiveArgPrefix(raw) && !queueReset && !hasOptions) { rawMode = token; consumed = i; } break; } return { consumed, queueMode, queueReset, rawMode, debounceMs, cap, dropPolicy, rawDebounce, rawCap, rawDrop, hasOptions }; } /** Extracts and removes a `/queue` directive from message text. */ function extractQueueDirective(body) { if (!body) return { cleaned: "", hasDirective: false, queueReset: false, hasOptions: false }; const match = /(?:^|\s)\/queue(?=$|\s|:)/i.exec(body); if (!match) return { cleaned: body.trim(), hasDirective: false, queueReset: false, hasOptions: false }; const start = match.index + match[0].indexOf("/queue"); const argsStart = start + 6; const parsed = parseQueueDirectiveArgs(body.slice(argsStart)); return { cleaned: `${body.slice(0, start)} ${body.slice(argsStart + parsed.consumed)}`.replace(/\s+/g, " ").trim(), queueMode: parsed.queueMode, queueReset: parsed.queueReset, rawMode: parsed.rawMode, debounceMs: parsed.debounceMs, cap: parsed.cap, dropPolicy: parsed.dropPolicy, rawDebounce: parsed.rawDebounce, rawCap: parsed.rawCap, rawDrop: parsed.rawDrop, hasDirective: true, hasOptions: parsed.hasOptions }; } //#endregion //#region src/utils/queue-helpers.ts /** Clear accumulated overflow summary state after it has been emitted. */ function clearQueueSummaryState(state) { state.droppedCount = 0; state.summaryLines = []; } /** Build a summary prompt preview without mutating the source queue state. */ function previewQueueSummaryPrompt(params) { return buildQueueSummaryPrompt({ state: { dropPolicy: params.state.dropPolicy, droppedCount: params.state.droppedCount, summaryLines: [...params.state.summaryLines] }, noun: params.noun, title: params.title }); } /** Apply runtime queue settings while preserving previous values for omitted fields. */ function applyQueueRuntimeSettings(params) { params.target.mode = params.settings.mode; params.target.debounceMs = typeof params.settings.debounceMs === "number" ? Math.max(0, params.settings.debounceMs) : params.target.debounceMs; params.target.cap = typeof params.settings.cap === "number" && params.settings.cap > 0 ? Math.floor(params.settings.cap) : params.target.cap; params.target.dropPolicy = params.settings.dropPolicy ?? params.target.dropPolicy; } /** Trim queue summary text to a bounded single-line preview. */ function elideQueueText(text, limit = 140) { if (text.length <= limit) return text; return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…`; } /** Normalize whitespace and elide one dropped item for queue summaries. */ function buildQueueSummaryLine(text, limit = 160) { return elideQueueText(text.replace(/\s+/g, " ").trim(), limit); } /** Run optional duplicate detection before an item enters a queue. */ function shouldSkipQueueItem(params) { if (!params.dedupe) return false; return params.dedupe(params.item, params.items); } /** Apply overflow policy before enqueueing another item. */ function applyQueueDropPolicy(params) { const cap = params.queue.cap; if (cap <= 0 || params.queue.items.length < cap) return true; if (params.queue.dropPolicy === "new") return false; const dropCount = params.queue.items.length - cap + 1; const dropped = params.queue.items.splice(0, dropCount); params.onDrop?.(dropped); if (params.queue.dropPolicy === "summarize") { for (const item of dropped) { params.queue.droppedCount += 1; params.queue.summaryLines.push(buildQueueSummaryLine(params.summarize(item))); } const limit = Math.max(0, params.summaryLimit ?? cap); while (params.queue.summaryLines.length > limit) params.queue.summaryLines.shift(); } return true; } /** Wait until the queue has been quiet for its debounce window. */ function waitForQueueDebounce(queue) { if (process.env.OPENCLAW_TEST_FAST === "1") return Promise.resolve(); const debounceMs = Math.max(0, queue.debounceMs); if (debounceMs <= 0) return Promise.resolve(); return new Promise((resolve) => { const check = () => { const since = Date.now() - queue.lastEnqueuedAt; if (since >= debounceMs) { resolve(); return; } setTimeout(check, debounceMs - since); }; check(); }); } /** Mark one queue as draining unless another drain is already active. */ function beginQueueDrain(map, key) { const queue = map.get(key); if (!queue || queue.draining) return; queue.draining = true; return queue; } function removeQueuedItemsByRef(items, processed) { for (const item of processed) { const idx = items.indexOf(item); if (idx !== -1) items.splice(idx, 1); } } /** Run and remove the next queued item, returning false when empty. */ async function drainNextQueueItem(items, run) { const next = items[0]; if (!next) return false; await run(next); removeQueuedItemsByRef(items, [next]); return true; } /** Drain one item when collect mode requires individual processing. */ async function drainCollectItemIfNeeded(params) { if (!params.forceIndividualCollect && !params.isCrossChannel) return "skipped"; if (params.isCrossChannel) params.setForceIndividualCollect?.(true); return await drainNextQueueItem(params.items, params.run) ? "drained" : "empty"; } /** Drain one collect step using mutable queue collection state. */ async function drainCollectQueueStep(params) { return await drainCollectItemIfNeeded({ forceIndividualCollect: params.collectState.forceIndividualCollect, isCrossChannel: params.isCrossChannel, setForceIndividualCollect: (next) => { params.collectState.forceIndividualCollect = next; }, items: params.items, run: params.run }); } /** Build and consume the queue overflow summary prompt. */ function buildQueueSummaryPrompt(params) { if (params.state.dropPolicy !== "summarize" || params.state.droppedCount <= 0) return; const noun = params.noun; const lines = [params.title ?? `[Queue overflow] Dropped ${params.state.droppedCount} ${noun}${params.state.droppedCount === 1 ? "" : "s"} due to cap.`]; if (params.state.summaryLines.length > 0) { lines.push("Summary:"); for (const line of params.state.summaryLines) lines.push(`- ${line}`); } clearQueueSummaryState(params.state); return lines.join("\n"); } /** Render a collect prompt from queued items and optional overflow summary. */ function buildCollectPrompt(params) { const blocks = [params.title]; if (params.summary) blocks.push(params.summary); params.items.forEach((item, idx) => { blocks.push(params.renderItem(item, idx)); }); return blocks.join("\n\n"); } /** Return true when queued items span keys or explicitly mark cross-channel state. */ function hasCrossChannelItems(items, resolveKey) { const keys = /* @__PURE__ */ new Set(); for (const item of items) { const resolved = resolveKey(item); if (resolved.cross) return true; if (!resolved.key) continue; keys.add(resolved.key); } return keys.size > 1; } //#endregion //#region src/auto-reply/reply/queue/types.ts var FollowupRunDeferredError = class extends Error { constructor(message = "Follow-up run deferred") { super(message); this.name = "FollowupRunDeferredError"; } }; function isFollowupRunDeferredError(error) { return error instanceof FollowupRunDeferredError; } function isFollowupRunAborted(run) { return run.abortSignal?.aborted === true; } const enqueuedFollowupLifecycles = /* @__PURE__ */ new WeakSet(); const completedFollowupLifecycles = /* @__PURE__ */ new WeakSet(); function markFollowupRunEnqueued(run) { const lifecycle = run.queuedLifecycle; if (!lifecycle || enqueuedFollowupLifecycles.has(lifecycle)) return; enqueuedFollowupLifecycles.add(lifecycle); lifecycle.onEnqueued?.(); } function completeFollowupRunLifecycle(run) { const lifecycle = run.queuedLifecycle; if (!lifecycle || completedFollowupLifecycles.has(lifecycle)) return; completedFollowupLifecycles.add(lifecycle); lifecycle.onComplete?.(); } const FOLLOWUP_QUEUES = resolveGlobalMap(Symbol.for("openclaw.followupQueues")); function getExistingFollowupQueue(key) { const cleaned = key.trim(); if (!cleaned) return; return FOLLOWUP_QUEUES.get(cleaned); } function getFollowupQueue(key, settings) { const existing = FOLLOWUP_QUEUES.get(key); if (existing) { applyQueueRuntimeSettings({ target: existing, settings }); return existing; } const created = { items: [], draining: false, lastEnqueuedAt: 0, mode: settings.mode, debounceMs: typeof settings.debounceMs === "number" ? Math.max(0, settings.debounceMs) : 500, cap: typeof settings.cap === "number" && settings.cap > 0 ? Math.floor(settings.cap) : 20, dropPolicy: settings.dropPolicy ?? "summarize", droppedCount: 0, summaryLines: [], summarySources: [] }; applyQueueRuntimeSettings({ target: created, settings }); FOLLOWUP_QUEUES.set(key, created); return created; } function clearFollowupQueue(key) { const cleaned = key.trim(); const queue = getExistingFollowupQueue(cleaned); if (!queue) return 0; const cleared = queue.items.length + queue.droppedCount; for (const item of queue.items) completeFollowupRunLifecycle(item); for (const item of queue.summarySources) completeFollowupRunLifecycle(item); queue.items.length = 0; queue.droppedCount = 0; queue.summaryLines = []; queue.summarySources = []; queue.lastRun = void 0; queue.lastEnqueuedAt = 0; FOLLOWUP_QUEUES.delete(cleaned); return cleared; } function refreshQueuedFollowupSession(params) { const cleaned = params.key.trim(); if (!cleaned) return; const queue = getExistingFollowupQueue(cleaned); if (!queue) return; const shouldRewriteSession = Boolean(params.previousSessionId) && Boolean(params.nextSessionId) && params.previousSessionId !== params.nextSessionId; const shouldRewriteModelSelection = typeof params.nextProvider === "string" || typeof params.nextModel === "string" || Object.hasOwn(params, "nextModelOverrideSource"); const shouldRewriteSelection = shouldRewriteModelSelection || Object.hasOwn(params, "nextAuthProfileId") || Object.hasOwn(params, "nextAuthProfileIdSource"); if (!shouldRewriteSession && !shouldRewriteSelection) return; const rewriteRun = (run) => { if (!run) return; if (shouldRewriteSession && run.sessionId === params.previousSessionId) { run.sessionId = params.nextSessionId; const nextSessionFile = normalizeOptionalString(params.nextSessionFile); if (nextSessionFile) run.sessionFile = nextSessionFile; } if (shouldRewriteSelection) { if (typeof params.nextProvider === "string") run.provider = params.nextProvider; if (typeof params.nextModel === "string") run.model = params.nextModel; if (shouldRewriteModelSelection) delete run.hasAutoFallbackProvenance; if (Object.hasOwn(params, "nextModelOverrideSource")) { run.hasSessionModelOverride = Boolean(run.provider || run.model); run.modelOverrideSource = params.nextModelOverrideSource; } if (Object.hasOwn(params, "nextAuthProfileId")) run.authProfileId = normalizeOptionalString(params.nextAuthProfileId); if (Object.hasOwn(params, "nextAuthProfileIdSource")) run.authProfileIdSource = run.authProfileId ? params.nextAuthProfileIdSource : void 0; } }; rewriteRun(queue.lastRun); for (const item of queue.items) rewriteRun(item.run); } //#endregion //#region src/auto-reply/reply/queue/drain.ts const FOLLOWUP_RUN_CALLBACKS = resolveGlobalMap(Symbol.for("openclaw.followupDrainCallbacks")); function rememberFollowupDrainCallback(key, runFollowup) { FOLLOWUP_RUN_CALLBACKS.set(key, runFollowup); } function clearFollowupDrainCallback(key) { FOLLOWUP_RUN_CALLBACKS.delete(key); } /** Restart the drain for `key` if it is currently idle, using the stored callback. */ function kickFollowupDrainIfIdle(key) { const cb = FOLLOWUP_RUN_CALLBACKS.get(key); if (!cb) return; scheduleFollowupDrain(key, cb); } function resolveOriginRoutingMetadata(items) { const metadata = {}; for (const item of items) { if (!metadata.originatingChannel && item.originatingChannel) metadata.originatingChannel = item.originatingChannel; if (!metadata.originatingTo && item.originatingTo) metadata.originatingTo = item.originatingTo; if (!metadata.originatingAccountId && item.originatingAccountId) metadata.originatingAccountId = item.originatingAccountId; if (metadata.originatingThreadId == null && item.originatingThreadId != null && item.originatingThreadId !== "") metadata.originatingThreadId = item.originatingThreadId; if (metadata.originatingChannel && metadata.originatingTo && metadata.originatingAccountId && metadata.originatingThreadId != null) break; } return metadata; } function resolveFollowupAuthorizationKey(run) { return JSON.stringify([ run.senderId ?? "", run.senderE164 ?? "", run.senderIsOwner === true, run.execOverrides?.host ?? "", run.execOverrides?.security ?? "", run.execOverrides?.ask ?? "", run.execOverrides?.node ?? "", run.bashElevated?.enabled === true, run.bashElevated?.allowed === true, run.bashElevated?.defaultLevel ?? "" ]); } function splitCollectItemsByAuthorization(items) { if (items.length <= 1) return items.length === 0 ? [] : [items]; const groups = []; let currentGroup = []; let currentKey; for (const item of items) { const itemKey = resolveFollowupAuthorizationKey(item.run); if (currentGroup.length === 0 || itemKey === currentKey) { currentGroup.push(item); currentKey = itemKey; continue; } groups.push(currentGroup); currentGroup = [item]; currentKey = itemKey; } if (currentGroup.length > 0) groups.push(currentGroup); return groups; } function renderCollectItem(item, idx) { const senderLabel = item.run.senderName ?? item.run.senderUsername ?? item.run.senderId ?? item.run.senderE164; const senderSuffix = senderLabel ? ` (from ${senderLabel})` : ""; return `---\nQueued #${idx + 1}${senderSuffix}\n${item.prompt}`.trim(); } function collectQueuedImages(items) { const images = []; const imageOrder = []; for (const item of items) { if (item.images) images.push(...item.images); if (item.imageOrder) imageOrder.push(...item.imageOrder); } return { ...images.length > 0 ? { images } : {}, ...imageOrder.length > 0 ? { imageOrder } : {} }; } function hasCurrentTurnRuntimeMetadata(item) { return item.currentInboundEventKind === "room_event" || item.currentInboundAudio === true || Boolean(item.currentInboundContext); } function hasRuntimeOnlyFollowupMetadata(item) { return Boolean(hasCurrentTurnRuntimeMetadata(item) || item.abortSignal || item.deliveryCorrelations?.length || item.queuedLifecycle); } function combineAbortSignals(items) { const signals = items.flatMap((item) => item.abortSignal ? [item.abortSignal] : []); if (signals.length === 0) return; if (signals.length === 1) return signals[0]; const nativeAny = AbortSignal.any; if (nativeAny) return nativeAny(signals); const controller = new AbortController(); const abort = () => controller.abort(); for (const signal of signals) { if (signal.aborted) { abort(); break; } signal.addEventListener("abort", abort, { once: true }); } return controller.signal; } function collectRuntimeMetadata(items, singletonOwner) { const candidates = singletonOwner ? [singletonOwner, ...items] : items; const currentTurnSource = singletonOwner && hasCurrentTurnRuntimeMetadata(singletonOwner) ? singletonOwner : items.find(hasCurrentTurnRuntimeMetadata); const abortSignal = singletonOwner?.abortSignal ?? combineAbortSignals(candidates); const deliveryCorrelations = items.flatMap((item) => item.deliveryCorrelations ?? []); const lifecycleSource = singletonOwner ?? items.find((item) => item.queuedLifecycle); return { currentInboundEventKind: currentTurnSource?.currentInboundEventKind, currentInboundAudio: currentTurnSource?.currentInboundAudio, currentInboundContext: currentTurnSource?.currentInboundContext, abortSignal, deliveryCorrelations: deliveryCorrelations.length > 0 ? deliveryCorrelations : void 0, queuedLifecycle: singletonOwner?.queuedLifecycle ?? (items.length === 1 ? lifecycleSource?.queuedLifecycle : void 0) }; } function collectSummaryRuntimeMetadata(items) { return collectRuntimeMetadata(items, items.length === 1 ? items[0] : void 0); } function clearFollowupQueueSummaryState(queue) { completeFollowupQueueSummarySources(queue); clearQueueSummaryState(queue); } function completeFollowupQueueSummarySources(queue) { for (const item of queue.summarySources ?? []) completeFollowupRunLifecycle(item); if (queue.summarySources) queue.summarySources = []; } function previewRestorableQueueSummaryPrompt(params) { const snapshot = { droppedCount: params.state.droppedCount, summaryLines: [...params.state.summaryLines] }; const prompt = previewQueueSummaryPrompt(params); if (!prompt) return {}; return { prompt, restore: () => { const currentLines = params.state.summaryLines; if (params.state.droppedCount >= snapshot.droppedCount && snapshot.summaryLines.every((line, index) => currentLines[index] === line)) return; params.state.droppedCount = params.state.droppedCount >= snapshot.droppedCount ? params.state.droppedCount : params.state.droppedCount + snapshot.droppedCount; params.state.summaryLines = [...snapshot.summaryLines, ...currentLines]; } }; } async function runWithSummarySourceCleanup(queue, run) { try { await run(); } catch (err) { if (!isFollowupRunDeferredError(err)) completeFollowupQueueSummarySources(queue); throw err; } completeFollowupQueueSummarySources(queue); } async function runWithDeferredSummaryRestore(restore, run) { try { return await run(); } catch (err) { if (isFollowupRunDeferredError(err)) restore?.(); throw err; } } async function dropAbortedFollowups(items, runFollowup) { let dropped = 0; for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if (isFollowupRunAborted(item)) { await runFollowup(item); completeFollowupRunLifecycle(item); items.splice(index, 1); dropped += 1; } } return dropped; } function resolveCrossChannelKey(item) { const { originatingChannel: channel, originatingTo: to, originatingAccountId: accountId } = item; const threadId = item.originatingThreadId; if (!channel && !to && !accountId && (threadId == null || threadId === "")) return {}; if (!isRoutableChannel(channel) || !to) return { cross: true }; const key = channelRouteCompactKey({ channel, to, accountId, threadId }); return key ? { key } : { cross: true }; } function scheduleFollowupDrain(key, runFollowup) { const queue = beginQueueDrain(FOLLOWUP_QUEUES, key); if (!queue) return; const effectiveRunFollowup = FOLLOWUP_RUN_CALLBACKS.get(key) ?? runFollowup; rememberFollowupDrainCallback(key, effectiveRunFollowup); (async () => { let retryDeferred = false; try { const collectState = { forceIndividualCollect: false }; while (queue.items.length > 0 || queue.droppedCount > 0) { if (await dropAbortedFollowups(queue.items, effectiveRunFollowup) > 0 && queue.items.length === 0) clearFollowupQueueSummaryState(queue); if (queue.items.length === 0 && queue.droppedCount === 0) break; await waitForQueueDebounce(queue); if (await dropAbortedFollowups(queue.items, effectiveRunFollowup) > 0 && queue.items.length === 0) clearFollowupQueueSummaryState(queue); if (queue.items.length === 0 && queue.droppedCount === 0) break; if (queue.mode === "collect") { const isCrossChannel = hasCrossChannelItems(queue.items, resolveCrossChannelKey) || queue.items.some(hasRuntimeOnlyFollowupMetadata); if (collectState.forceIndividualCollect && !isCrossChannel && queue.items.length > 1) collectState.forceIndividualCollect = false; const collectDrainResult = await drainCollectQueueStep({ collectState, isCrossChannel, items: queue.items, run: effectiveRunFollowup }); if (collectDrainResult === "empty") { const summaryOnly = previewRestorableQueueSummaryPrompt({ state: queue, noun: "message" }); const summaryOnlyPrompt = summaryOnly.prompt; const run = queue.lastRun; if (summaryOnlyPrompt && run) { await runWithDeferredSummaryRestore(summaryOnly.restore, async () => { await runWithSummarySourceCleanup(queue, async () => { await effectiveRunFollowup({ prompt: summaryOnlyPrompt, run, enqueuedAt: Date.now(), ...collectSummaryRuntimeMetadata([]), ...collectQueuedImages(queue.items) }); }); }); clearFollowupQueueSummaryState(queue); continue; } summaryOnly.restore?.(); break; } if (collectDrainResult === "drained") continue; const items = queue.items.slice(); const summaryResult = previewRestorableQueueSummaryPrompt({ state: queue, noun: "message" }); const summary = summaryResult.prompt; const authGroups = splitCollectItemsByAuthorization(items); if (authGroups.length === 0) { const run = queue.lastRun; if (!summary || !run) { summaryResult.restore?.(); break; } await runWithDeferredSummaryRestore(summaryResult.restore, async () => { await runWithSummarySourceCleanup(queue, async () => { await effectiveRunFollowup({ prompt: summary, run, enqueuedAt: Date.now(), ...collectSummaryRuntimeMetadata([]) }); }); }); clearFollowupQueueSummaryState(queue); continue; } let pendingSummary = summary; for (const groupItems of authGroups) { const run = groupItems.at(-1)?.run ?? queue.lastRun; if (!run) break; const routing = resolveOriginRoutingMetadata(groupItems); const prompt = buildCollectPrompt({ title: "[Queued messages while agent was busy]", items: groupItems, summary: pendingSummary, renderItem: renderCollectItem }); const drainGroup = async () => { await effectiveRunFollowup({ prompt, run, enqueuedAt: Date.now(), ...routing, ...collectRuntimeMetadata(groupItems), ...collectQueuedImages(groupItems) }); }; if (pendingSummary) await runWithDeferredSummaryRestore(summaryResult.restore, async () => { await runWithSummarySourceCleanup(queue, drainGroup); }); else await drainGroup(); removeQueuedItemsByRef(queue.items, groupItems); if (pendingSummary) { clearFollowupQueueSummaryState(queue); pendingSummary = void 0; } } continue; } const summaryResult = previewRestorableQueueSummaryPrompt({ state: queue, noun: "message" }); const summaryPrompt = summaryResult.prompt; if (summaryPrompt) { const run = queue.lastRun; if (!run) { summaryResult.restore?.(); break; } if (!await runWithDeferredSummaryRestore(summaryResult.restore, async () => drainNextQueueItem(queue.items, async (item) => { await runWithSummarySourceCleanup(queue, async () => { await effectiveRunFollowup({ prompt: summaryPrompt, run, enqueuedAt: Date.now(), originatingChannel: item.originatingChannel, originatingTo: item.originatingTo, originatingAccountId: item.originatingAccountId, originatingThreadId: item.originatingThreadId, ...collectSummaryRuntimeMetadata([item]), ...collectQueuedImages([item]) }); }); }))) break; clearFollowupQueueSummaryState(queue); continue; } if (!await drainNextQueueItem(queue.items, effectiveRunFollowup)) break; } } catch (err) { queue.lastEnqueuedAt = Date.now(); if (isFollowupRunDeferredError(err)) retryDeferred = true; else defaultRuntime.error?.(`followup queue drain failed for ${key}: ${String(err)}`); } finally { queue.draining = false; const hasPendingQueueWork = queue.items.length > 0 || queue.droppedCount > 0; if (retryDeferred && hasPendingQueueWork) scheduleFollowupDrain(key, effectiveRunFollowup); else if (!hasPendingQueueWork) { if (FOLLOWUP_QUEUES.get(key) === queue) { FOLLOWUP_QUEUES.delete(key); clearFollowupDrainCallback(key); } } else scheduleFollowupDrain(key, effectiveRunFollowup); } })(); } //#endregion //#region src/auto-reply/reply/queue/cleanup.ts const defaultQueueCleanupDeps = { resolveEmbeddedSessionLane, clearCommandLane }; const queueCleanupDeps = { ...defaultQueueCleanupDeps }; function resolveQueueCleanupLaneResolver() { return typeof queueCleanupDeps.resolveEmbeddedSessionLane === "function" ? queueCleanupDeps.resolveEmbeddedSessionLane : defaultQueueCleanupDeps.resolveEmbeddedSessionLane; } function resolveQueueCleanupLaneClearer() { return typeof queueCleanupDeps.clearCommandLane === "function" ? queueCleanupDeps.clearCommandLane : defaultQueueCleanupDeps.clearCommandLane; } function clearSessionQueues(keys) { const seen = /* @__PURE__ */ new Set(); let followupCleared = 0; let laneCleared = 0; const clearedKeys = []; const resolveLane = resolveQueueCleanupLaneResolver(); const clearLane = resolveQueueCleanupLaneClearer(); for (const key of keys) { const cleaned = normalizeOptionalString(key); if (!cleaned || seen.has(cleaned)) continue; seen.add(cleaned); clearedKeys.push(cleaned); followupCleared += clearFollowupQueue(cleaned); clearFollowupDrainCallback(cleaned); laneCleared += clearLane(resolveLane(cleaned)); } return { followupCleared, laneCleared, keys: clearedKeys }; } //#endregion //#region src/auto-reply/reply/queue/enqueue.ts const RECENT_QUEUE_MESSAGE_IDS = resolveGlobalDedupeCache(Symbol.for("openclaw.recentQueueMessageIds"), { ttlMs: 300 * 1e3, maxSize: 1e4 }); function followupRouteIdentityKey(run) { return channelRouteDedupeKey({ channel: run.originatingChannel, to: run.originatingTo, accountId: run.originatingAccountId, threadId: run.originatingThreadId }); } function buildRecentMessageIdKey(run, queueKey) { const messageId = normalizeOptionalString(run.messageId); if (!messageId) return; return JSON.stringify([ "queue", queueKey, followupRouteIdentityKey(run), messageId ]); } function isRunAlreadyQueued(run, items, allowPromptFallback = false) { const routeKey = followupRouteIdentityKey(run); const hasSameRouting = (item) => followupRouteIdentityKey(item) === routeKey; const messageId = normalizeOptionalString(run.messageId); if (messageId) return items.some((item) => normalizeOptionalString(item.messageId) === messageId && hasSameRouting(item)); if (!allowPromptFallback) return false; return items.some((item) => item.prompt === run.prompt && hasSameRouting(item)); } function enqueueFollowupRun(key, run, settings, dedupeMode = "message-id", runFollowup, restartIfIdle = true) { if (isFollowupRunAborted(run)) return false; const queue = getFollowupQueue(key, settings); const recentMessageIdKey = dedupeMode !== "none" ? buildRecentMessageIdKey(run, key) : void 0; if (recentMessageIdKey && RECENT_QUEUE_MESSAGE_IDS.peek(recentMessageIdKey)) return false; const dedupe = dedupeMode === "none" ? void 0 : (item, items) => isRunAlreadyQueued(item, items, dedupeMode === "prompt"); if (shouldSkipQueueItem({ item: run, items: queue.items, dedupe })) return false; queue.lastEnqueuedAt = Date.now(); queue.lastRun = run.run; const shouldEnqueue = applyQueueDropPolicy({ queue, summarize: (item) => normalizeOptionalString(item.summaryLine) || item.prompt.trim(), onDrop: (dropped) => { if (queue.dropPolicy === "summarize") { queue.summarySources.push(...dropped); return; } for (const item of dropped) completeFollowupRunLifecycle(item); } }); if (queue.dropPolicy === "summarize") { const overflow = queue.summarySources.length - queue.summaryLines.length; if (overflow > 0) { const removed = queue.summarySources.splice(0, overflow); for (const item of removed) completeFollowupRunLifecycle(item); } } if (!shouldEnqueue) return false; queue.items.push(run); markFollowupRunEnqueued(run); if (recentMessageIdKey) RECENT_QUEUE_MESSAGE_IDS.check(recentMessageIdKey); if (runFollowup) rememberFollowupDrainCallback(key, runFollowup); if (restartIfIdle && !queue.draining) kickFollowupDrainIfIdle(key); return true; } function getFollowupQueueDepth(key) { const queue = getExistingFollowupQueue(key); if (!queue) return 0; return queue.items.length; } //#endregion //#region src/auto-reply/reply/queue/settings.ts function defaultQueueModeForChannel(_channel) { return "steer"; } /** Resolve per-channel debounce override from debounceMsByChannel map. */ function resolveChannelDebounce(byChannel, channelKey) { if (!channelKey || !byChannel) return; const value = byChannel[channelKey]; return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : void 0; } function resolveQueueSettings$1(params) { const channelKey = normalizeOptionalLowercaseString(params.channel); const queueCfg = params.cfg.messages?.queue; const providerModeRaw = channelKey && queueCfg?.byChannel ? queueCfg.byChannel[channelKey] : void 0; const resolvedMode = params.inlineMode ?? normalizePersistedQueueMode(params.sessionEntry?.queueMode) ?? normalizeQueueMode(providerModeRaw) ?? normalizeQueueMode(queueCfg?.mode) ?? defaultQueueModeForChannel(channelKey); const debounceRaw = params.inlineOptions?.debounceMs ?? params.sessionEntry?.queueDebounceMs ?? resolveChannelDebounce(queueCfg?.debounceMsByChannel, channelKey) ?? params.pluginDebounceMs ?? queueCfg?.debounceMs ?? 500; const capRaw = params.inlineOptions?.cap ?? params.sessionEntry?.queueCap ?? queueCfg?.cap ?? 20; const dropRaw = params.inlineOptions?.dropPolicy ?? params.sessionEntry?.queueDrop ?? normalizeQueueDropPolicy(queueCfg?.drop) ?? "summarize"; return { mode: resolvedMode, debounceMs: typeof debounceRaw === "number" ? Math.max(0, debounceRaw) : void 0, cap: typeof capRaw === "number" ? Math.max(1, Math.floor(capRaw)) : void 0, dropPolicy: dropRaw }; } //#endregion //#region src/auto-reply/reply/queue/settings-runtime.ts /** Resolves plugin-provided debounce defaults for a channel queue. */ function resolvePluginDebounce(channelKey) { if (!channelKey) return; const value = getLoadedChannelPlugin(channelKey)?.defaults?.queue?.debounceMs; return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : void 0; } /** Resolves queue settings with channel plugin defaults layered into core config. */ function resolveQueueSettings(params) { const channelKey = normalizeOptionalLowercaseString(params.channel); return resolveQueueSettings$1({ ...params, pluginDebounceMs: params.pluginDebounceMs ?? resolvePluginDebounce(channelKey) }); } //#endregion export { clearSessionQueues as a, FollowupRunDeferredError as c, extractQueueDirective as d, skipDirectiveArgPrefix as f, getFollowupQueueDepth as i, completeFollowupRunLifecycle as l, resolveQueueSettings$1 as n, scheduleFollowupDrain as o, takeDirectiveToken as p, enqueueFollowupRun as r, refreshQueuedFollowupSession as s, resolveQueueSettings as t, isFollowupRunAborted as u };