UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

398 lines (397 loc) • 17.3 kB
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs"; import { Et as _enum, Fn as object, Jn as string } from "./schemas-BOYIvvln.mjs"; import { i as jsonUtf8BytesOrInfinity, r as jsonUtf8Bytes } from "./json-utf8-bytes-C0-PtdPX.mjs"; import { n as readLegacyCompactionMetrics } from "./legacy-compaction-history-DS8GebGO.mjs"; import { a as readToolErrorFlag, i as isToolResultContentType, n as isToolCallContentType } from "./tool-content-SH_V-fgH.mjs"; import { a as readTranscriptDisplayPosition, i as composeTranscriptDisplay } from "./transcript-image-artifacts-BNfLH5hX.mjs"; import { a as readChatHistoryMessageSeq, i as readChatHistoryMessageId, o as readChatHistoryPaginationKey } from "./session-history-tail-Ct-5F_XJ.mjs"; import { t as logLargePayload } from "./diagnostic-payload-DEskObns.mjs"; //#region src/gateway/server-methods/chat-history-page-cursor.ts const PREFIX = "history-page:"; const MAX_CURSOR_CHARS = 4096; const cursorSchema = object({ sessionId: string().min(1).max(1024), source: string().min(1).max(128), messageId: string().min(1).max(1024), direction: _enum(["older", "newer"]) }); function encodeChatHistoryPageCursor(cursor) { if (!cursorSchema.safeParse(cursor).success) return; const encoded = PREFIX + Buffer.from(JSON.stringify(cursor)).toString("base64url"); return encoded.length <= MAX_CURSOR_CHARS ? encoded : void 0; } /** Undefined is a delta cursor; null is a malformed page cursor. Neither grants access. */ function decodeChatHistoryPageCursor(cursor) { if (!cursor?.startsWith(PREFIX)) return; if (cursor.length > MAX_CURSOR_CHARS) return null; try { const parsed = cursorSchema.safeParse(JSON.parse(Buffer.from(cursor.slice(13), "base64url").toString("utf8"))); return parsed.success ? parsed.data : null; } catch { return null; } } function resolveChatHistoryPageCursors(anchor, capped, original) { if (!anchor) return {}; const oldest = readChatHistoryMessageId(capped[0]) ?? anchor.oldestMessageId; const newest = readChatHistoryMessageId(capped.at(-1)) ?? anchor.newestMessageId; const cursor = (messageId, direction) => encodeChatHistoryPageCursor({ sessionId: anchor.sessionId, source: anchor.source, messageId, direction }); return { ...oldest && (anchor.hasOlder || capped.length > 0 && oldest !== readChatHistoryMessageId(original[0])) ? { olderCursor: cursor(oldest, "older") } : {}, ...newest && (anchor.hasNewer || capped.length > 0 && newest !== readChatHistoryMessageId(original.at(-1))) ? { newerCursor: cursor(newest, "newer") } : {} }; } //#endregion //#region src/gateway/server-methods/chat-history-budget.ts const CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES = 131072; const CHAT_HISTORY_OVERSIZED_PLACEHOLDER = "[chat.history omitted: message too large]"; const CHAT_HISTORY_UNAVAILABLE_SENTINEL = "[chat.history unavailable: transcript too large to display; the full history is preserved on disk]"; let chatHistoryOmittedEmitCount = 0; function createChatHistoryActivityProjection(messages, activity = []) { const byId = new Map(activity.map((entry) => [entry.messageId, entry])); return new Map(messages.flatMap((message) => { const messageId = readChatHistoryMessageId(message); const entry = messageId ? byId.get(messageId) : void 0; const record = asOptionalRecord(message); const toolBearing = record && (isToolResultContentType(record.role) || record.role === "tool" || record.role === "function" || Array.isArray(record.content) && record.content.some((block) => { const type = asOptionalRecord(block)?.type; return isToolCallContentType(type) || isToolResultContentType(type); })); return entry && toolBearing ? [[message, entry]] : []; })); } function createChatHistoryByteCounter(activity) { const sizes = /* @__PURE__ */ new Map(); const messageBytes = (message) => { const cached = sizes.get(message); if (cached !== void 0) return cached; const descriptor = activity?.get(message); const bytes = jsonUtf8Bytes(message) + (descriptor ? jsonUtf8Bytes(descriptor) + 1 : 0); sizes.set(message, bytes); return bytes; }; return { messageBytes, framingBytes: (messages) => messages.some((message) => activity?.has(message)) ? 13 : 0, messagesBytes: (messages) => (messages.some((message) => activity?.has(message)) ? 15 : 2) + messages.reduce((bytes, message) => bytes + messageBytes(message), 0) + Math.max(0, messages.length - 1) }; } function chatHistoryActivityBytes(activity) { return activity.length > 0 ? jsonUtf8Bytes({ activity }) - 1 : 0; } /** Delta envelopes share one prepared plain-data snapshot throughout their synchronous projection. */ function createChatHistoryDeltaByteCounter(sessionSnapshot) { let snapshot; return (envelope) => { snapshot ??= { bytes: jsonUtf8BytesOrInfinity(sessionSnapshot), keys: new Set(Object.keys(sessionSnapshot)) }; const fields = {}; for (const key in envelope) if (!snapshot.keys.has(key) && Object.hasOwn(envelope, key)) fields[key] = envelope[key]; const fieldsBytes = jsonUtf8BytesOrInfinity(fields); return snapshot.bytes + fieldsBytes - 2 + (snapshot.bytes > 2 && fieldsBytes > 2 ? 1 : 0); }; } function buildChatHistoryUnavailableSentinel() { return { role: "assistant", timestamp: Date.now(), content: [{ type: "text", text: CHAT_HISTORY_UNAVAILABLE_SENTINEL }] }; } function buildOversizedHistoryPlaceholder(message) { const entry = asOptionalRecord(message) ?? {}; const role = typeof entry.role === "string" ? entry.role : "assistant"; const timestamp = typeof entry.timestamp === "number" ? entry.timestamp : Date.now(); const metadata = asOptionalRecord(entry["__openclaw"]) ?? {}; const toolIdentity = Object.fromEntries([ "toolCallId", "tool_call_id", "toolUseId", "tool_use_id", "toolName", "tool_name", "name" ].filter((key) => typeof entry[key] === "string").map((key) => [key, entry[key]])); const isError = readToolErrorFlag(entry); const metadataId = typeof metadata.id === "string" ? metadata.id : void 0; const metadataSeq = typeof metadata.seq === "number" ? metadata.seq : void 0; const metadataIdempotencyKey = typeof metadata.idempotencyKey === "string" ? metadata.idempotencyKey : void 0; const turnBoundary = metadata.turnBoundary === true; const transcriptPosition = readTranscriptDisplayPosition(metadata.transcriptPosition); return { role, timestamp, content: [{ type: "text", text: CHAT_HISTORY_OVERSIZED_PLACEHOLDER }], ...toolIdentity, ...isError !== void 0 ? { isError } : {}, __openclaw: { ...metadata.toolOutput ? { toolOutput: metadata.toolOutput } : {}, ...metadataId ? { id: metadataId } : {}, ...typeof metadata.runId === "string" ? { runId: metadata.runId } : {}, ...metadataSeq !== void 0 ? { seq: metadataSeq } : {}, ...metadataIdempotencyKey ? { idempotencyKey: metadataIdempotencyKey } : {}, ...turnBoundary ? { turnBoundary: true } : {}, ...transcriptPosition ? { transcriptPosition } : {}, truncated: true, reason: "oversized" } }; } function replaceOversizedChatHistoryMessages(params) { const { messages, maxSingleMessageBytes } = params; const byteCounter = params.byteCounter ?? createChatHistoryByteCounter(); let replacedCount = 0; const next = messages.map((message) => { if (byteCounter.messageBytes(message) <= maxSingleMessageBytes) return message; replacedCount += 1; const placeholder = buildOversizedHistoryPlaceholder(message); return byteCounter.messageBytes(placeholder) <= maxSingleMessageBytes ? placeholder : buildChatHistoryUnavailableSentinel(); }); return { messages: replacedCount > 0 ? next : messages, replacedCount }; } function reportOmittedChatHistory(params) { const { omittedCount, normalizedBytes, maxHistoryBytes, logDebug } = params; if (omittedCount === 0) return 0; chatHistoryOmittedEmitCount += omittedCount; logLargePayload({ surface: "gateway.chat.history", action: "truncated", bytes: normalizedBytes, limitBytes: maxHistoryBytes, count: omittedCount, reason: "chat_history_budget" }); logDebug(`chat.history omitted oversized payloads count=${omittedCount} total=${chatHistoryOmittedEmitCount}`); return omittedCount; } function boundInFlightRunSnapshotForChatHistory(params) { if (!params.snapshot) return; const messagesBytes = params.getMessagesBytes?.() ?? jsonUtf8Bytes(params.messages); if (messagesBytes + jsonUtf8Bytes(params.snapshot) <= params.maxBytes) return params.snapshot; let bounded = { runId: params.snapshot.runId, text: "", ...params.snapshot.sessionAbortable ? { sessionAbortable: true } : {}, ...params.snapshot.events ? { events: [] } : {}, ...params.snapshot.plan ? { plan: { steps: [] } } : {} }; const retainIfWithinBudget = (candidate) => { if (!(messagesBytes + jsonUtf8Bytes(candidate) <= params.maxBytes)) return false; bounded = candidate; return true; }; if (params.snapshot.startedAt !== void 0) retainIfWithinBudget({ ...bounded, startedAt: params.snapshot.startedAt }); if (params.snapshot.events) { const events = params.snapshot.events; let start = 0; let end = events.length; let middle = 0; while (start < end) { if (retainIfWithinBudget({ ...bounded, events: events.slice(middle) })) end = middle; else start = middle + 1; middle = Math.floor((start + end) / 2); } } if (params.snapshot.plan) retainIfWithinBudget({ ...bounded, plan: params.snapshot.plan }); if (params.snapshot.text) retainIfWithinBudget({ ...bounded, text: params.snapshot.text }); return bounded; } //#endregion //#region src/gateway/server-methods/chat-history-response-page.ts function readPageMessageSequence(message, messageSequences) { if (typeof messageSequences === "function") return messageSequences(message); return messageSequences?.[readChatHistoryPaginationKey(message) ?? ""] ?? readChatHistoryMessageSeq(message); } function resolveChatHistoryNextOffset(params) { const sequence = (message) => readPageMessageSequence(message, params.messageSequences); let oldestSeq; for (const message of params.messages) { oldestSeq = sequence(message); if (oldestSeq !== void 0) break; } if (oldestSeq === void 0) return params.offset + params.rawPageMessages; const recordOffset = params.totalMessages - oldestSeq + 1; return Math.max(params.offset + 1, recordOffset); } /** Preserve token metrics saved by pre-removal builds; new markers own their metrics. */ function enrichChatHistoryCompactionMarkers(messages, entry, metrics = readLegacyCompactionMetrics(entry)) { if (metrics.length === 0) return messages; const checkpointByEntryId = new Map(metrics.map((metric) => [metric.entryId, metric])); let changed = false; const enriched = messages.map((message) => { const record = asOptionalRecord(message); const metadata = asOptionalRecord(record?.["__openclaw"]); if (metadata?.kind !== "compaction" || typeof metadata.id !== "string") return message; const checkpoint = checkpointByEntryId.get(metadata.id); if (!checkpoint) return message; const tokensBefore = checkpoint.tokensBefore; const tokensAfter = checkpoint.tokensAfter; if (tokensBefore === void 0 && tokensAfter === void 0) return message; changed = true; return { ...record, __openclaw: { ...metadata, ...tokensBefore !== void 0 ? { tokensBefore } : {}, ...tokensAfter !== void 0 ? { tokensAfter } : {} } }; }); return changed ? enriched : messages; } function resolveChatHistoryMessageGroup(messages, index, messageCost, messageSequences) { const sequence = (message) => readPageMessageSequence(message, messageSequences); const seq = sequence(messages[index]); let start = index; let end = index + 1; let cost = messageCost(messages[index]); if (seq === void 0) return { start, end, cost }; while (start > 0 && sequence(messages[start - 1]) === seq) { start -= 1; cost += messageCost(messages[start]); } while (end < messages.length && sequence(messages[end]) === seq) { cost += messageCost(messages[end]); end += 1; } return { start, end, cost }; } function capChatHistoryTail(params) { let start = params.messages.length; let cost = 0; while (start > 0) { const group = resolveChatHistoryMessageGroup(params.messages, start - 1, params.messageCost, params.messageSequences); if (cost + group.cost > params.maxCost) break; start = group.start; cost += group.cost; } return start > 0 ? params.messages.slice(start) : params.messages; } function capChatHistoryAroundMessage(params) { const anchorIndex = params.messages.findIndex((message) => readChatHistoryMessageId(message) === params.messageId); if (anchorIndex === -1) return []; const groupAt = (index) => resolveChatHistoryMessageGroup(params.messages, index, params.messageCost, params.messageSequences); const anchorGroup = groupAt(anchorIndex); if (!(anchorGroup.cost <= params.maxCost)) return [params.messages[anchorIndex]]; let { start, end, cost } = anchorGroup; const grow = (index) => { const group = groupAt(index); if (!(cost + group.cost <= params.maxCost)) return false; start = Math.min(start, group.start); end = Math.max(end, group.end); cost += group.cost; return true; }; let canGrowOlder = start > 0; let canGrowNewer = end < params.messages.length; while (canGrowOlder || canGrowNewer) { canGrowOlder &&= grow(start - 1) && start > 0; canGrowNewer &&= grow(end) && end < params.messages.length; } return params.messages.slice(start, end); } function prepareChatHistoryResponsePage(historyPage, { entry: historyEntry, compactionMetrics, maxHistoryBytes, responseHistoryBytes = maxHistoryBytes, messageId }, messageSequences = historyPage.pagination?.messageSequences ?? historyPage.anchor?.messageSequences) { const normalized = enrichChatHistoryCompactionMarkers(historyPage.messages, historyEntry, compactionMetrics); const activity = createChatHistoryActivityProjection(normalized, historyPage.activity); const byteCounter = createChatHistoryByteCounter(activity); const maxCost = responseHistoryBytes - (1 + byteCounter.framingBytes(normalized)); const messageCost = (message) => byteCounter.messageBytes(message) + 1; const groups = []; for (let index = 0; index < normalized.length;) { const group = resolveChatHistoryMessageGroup(normalized, index, messageCost, messageSequences); groups.push(...group.cost > maxCost ? [buildOversizedHistoryPlaceholder(normalized.slice(group.start, group.end).find((message) => readChatHistoryMessageId(message) === messageId) ?? normalized[group.end - 1])] : normalized.slice(group.start, group.end)); index = group.end; } const replaced = replaceOversizedChatHistoryMessages({ byteCounter, messages: groups, maxSingleMessageBytes: Math.min(CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES, maxCost - 1) }); const capParams = { messages: replaced.messages, maxCost, messageCost, messageSequences }; const capped = messageId ? capChatHistoryAroundMessage({ ...capParams, messageId: historyPage.anchor?.direction ? readChatHistoryMessageId(historyPage.anchor.direction === "newer" ? replaced.messages[0] : replaced.messages.at(-1)) ?? messageId : messageId }) : capChatHistoryTail(capParams); const pagination = historyPage.pagination; const candidateNextOffset = pagination === void 0 ? void 0 : resolveChatHistoryNextOffset({ messages: capped, totalMessages: pagination.totalMessages, offset: pagination.offset, rawPageMessages: pagination.rawPageMessages, messageSequences: pagination.messageSequences }); const hasMore = pagination !== void 0 && candidateNextOffset !== void 0 ? candidateNextOffset < pagination.totalMessages : void 0; const survivors = new Set(capped); const omittedCount = normalized.reduce((count, message) => count + (survivors.has(message) ? 0 : 1), 0); return { messages: composeTranscriptDisplay(capped), ...capped.some((message) => activity.has(message)) ? { activity: capped.flatMap((message) => activity.get(message) ?? []) } : {}, messagesBytes: byteCounter.messagesBytes(capped), ...omittedCount > 0 ? { omission: { omittedCount, normalizedBytes: byteCounter.messagesBytes(normalized), ...capped.length < replaced.messages.length ? { byteLimited: true } : {} } } : {}, responseHistoryBytes, ...resolveChatHistoryPageCursors(historyPage.anchor, capped, normalized), ...hasMore ? { nextOffset: candidateNextOffset } : {}, ...hasMore !== void 0 ? { hasMore } : {}, ...pagination !== void 0 ? { totalMessages: pagination.totalMessages } : {} }; } function encodeChatHistoryResponsePage(page, params) { if (!params.encodeResponse) return page; const response = prepareChatHistoryResponsePage(page, params); return { ...page, messages: [], activity: void 0, encodedResponse: { ...response, messages: new TextEncoder().encode(JSON.stringify(response.messages)) } }; } //#endregion export { boundInFlightRunSnapshotForChatHistory as a, createChatHistoryByteCounter as c, reportOmittedChatHistory as d, decodeChatHistoryPageCursor as f, CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES as i, createChatHistoryDeltaByteCounter as l, enrichChatHistoryCompactionMarkers as n, chatHistoryActivityBytes as o, prepareChatHistoryResponsePage as r, createChatHistoryActivityProjection as s, encodeChatHistoryResponsePage as t, replaceOversizedChatHistoryMessages as u };