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