openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
955 lines (954 loc) • 36.4 kB
JavaScript
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 };