openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
432 lines (431 loc) • 17.3 kB
JavaScript
import { s as asFiniteNumber } from "./number-coercion-CLj0HTDM.js";
import { a as asOptionalRecord, c as isRecord } from "./record-coerce-DItp3I4t.js";
import { g as readStringValue, l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { i as stripInboundMetadata } from "./strip-inbound-meta-D5F_RINt.js";
import { c as normalizeProviderId } from "./model-ref-shared-Dz7QU0Lx.js";
import { d as readPersistedMediaFacts, o as isImageMediaFact } from "./media-facts-8Sl_nPCc.js";
import { a as stripInlineDirectiveTagsForDisplay } from "./directive-tags-Wp1S72ap.js";
import { n as getCliSessionBinding, r as normalizeCliSessionReseedReceipt } from "./cli-session-binding-Aa6nlDa8.js";
import "./model-selection-di2kjKCB.js";
import { r as isOpenClawCliImageCachePath } from "./images.media-refs-DkoAIIi5.js";
import { n as hashCliImageTurnEntryId, r as readCliImageTurnContext } from "./cli-image-turn-correlation-Dxv2z8kT.js";
import { a as parseClaudeCliHistoryEntry, c as redactClaudeCliHistoryMessage, d as resolveClaudeCliSessionFilePath, i as decodeClaudeCliProjectEntry, n as appendCoalescedClaudeCliToolMessage, r as createClaudeReseedImportState, s as readClaudeCliSessionMessages, t as CLAUDE_CLI_PROVIDER } from "./cli-session-history.claude-5nFgZ0im.js";
import fs from "node:fs";
import { Worker } from "node:worker_threads";
import { setImmediate } from "node:timers/promises";
import readline from "node:readline";
//#region src/gateway/cli-session-history.claude-snapshot.ts
const YIELD_BYTES = 262144;
const OFFTHREAD_JSONL_LINE_CHARS = 1048576;
const OVERSIZED_HISTORY_PLACEHOLDER = "[Claude CLI history record omitted from context because it exceeded 1 MiB.]";
const OVERSIZED_ENTRY_WORKER_SOURCE = `
const { parentPort, workerData } = require("node:worker_threads");
const boundedString = (value, max) =>
typeof value === "string" && value.length <= max ? value : undefined;
try {
const entry = JSON.parse(workerData);
const type = entry?.type;
const message = entry?.message;
if ((type !== "user" && type !== "assistant") || !message || message.role !== type) {
parentPort.postMessage(null);
} else {
const rawUsage = message.usage;
const usage = rawUsage && typeof rawUsage === "object"
? Object.fromEntries(
["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"]
.flatMap((key) => Number.isFinite(rawUsage[key]) ? [[key, rawUsage[key]]] : []),
)
: undefined;
parentPort.postMessage({
type,
timestamp: boundedString(entry.timestamp, 128),
uuid: boundedString(entry.uuid, 1_024),
isSidechain: entry.isSidechain === true,
isMeta: entry.isMeta === true,
isCompactSummary: entry.isCompactSummary === true,
isVisibleInTranscriptOnly: entry.isVisibleInTranscriptOnly === true,
message: {
role: type,
content: ${JSON.stringify(OVERSIZED_HISTORY_PLACEHOLDER)},
model: boundedString(message.model, 256),
stop_reason: boundedString(message.stop_reason, 128),
usage,
},
});
}
} catch {
parentPort.postMessage(null);
}
`;
let snapshotCache;
function normalizeOversizedEntry(value) {
if (!isRecord(value) || value.type !== "user" && value.type !== "assistant") return null;
const message = value.message;
if (!isRecord(message) || message.role !== value.type) return null;
const usage = isRecord(message.usage) ? message.usage : void 0;
return {
type: value.type,
...typeof value.timestamp === "string" ? { timestamp: value.timestamp } : {},
...typeof value.uuid === "string" ? { uuid: value.uuid } : {},
...value.isSidechain === true ? { isSidechain: true } : {},
...value.isMeta === true ? { isMeta: true } : {},
...value.isCompactSummary === true ? { isCompactSummary: true } : {},
...value.isVisibleInTranscriptOnly === true ? { isVisibleInTranscriptOnly: true } : {},
message: {
role: value.type,
content: OVERSIZED_HISTORY_PLACEHOLDER,
...typeof message.model === "string" ? { model: message.model } : {},
...typeof message.stop_reason === "string" ? { stop_reason: message.stop_reason } : {},
...usage ? { usage: {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
cache_read_input_tokens: usage.cache_read_input_tokens,
cache_creation_input_tokens: usage.cache_creation_input_tokens
} } : {}
}
};
}
async function decodeOversizedClaudeEntry(line) {
let worker;
try {
worker = new Worker(OVERSIZED_ENTRY_WORKER_SOURCE, {
eval: true,
workerData: line
});
} catch {
return null;
}
return await new Promise((resolve) => {
let settled = false;
const finish = (value) => {
if (settled) return;
settled = true;
resolve(normalizeOversizedEntry(value));
};
worker.once("message", finish);
worker.once("error", () => finish(null));
worker.once("exit", () => finish(null));
});
}
function fingerprint(stats) {
return [
stats.dev,
stats.ino,
stats.size,
stats.mtimeMs,
stats.ctimeMs
].join(":");
}
async function resolveSource(params) {
const candidate = resolveClaudeCliSessionFilePath(params);
if (!candidate) return;
try {
const filePath = await fs.promises.realpath(candidate);
const sourceFingerprint = fingerprint(await fs.promises.stat(filePath));
return [filePath, JSON.stringify([
filePath,
sourceFingerprint,
params.cliSessionId,
params.localSessionId?.trim() || null,
normalizeCliSessionReseedReceipt(params.reseedReceipt)
])];
} catch {
return;
}
}
async function parseSnapshot(filePath, params) {
const messages = [];
const toolNames = /* @__PURE__ */ new Map();
const lines = readline.createInterface({
input: fs.createReadStream(filePath, { encoding: "utf8" }),
crlfDelay: Number.POSITIVE_INFINITY
});
const reseedState = createClaudeReseedImportState(params);
let bytesSinceYield = 0;
let lineNumber = 0;
for await (const line of lines) {
lineNumber += 1;
const oversized = line.length > OFFTHREAD_JSONL_LINE_CHARS;
if (oversized) bytesSinceYield = 0;
else {
bytesSinceYield += Buffer.byteLength(line, "utf8") + 1;
if (bytesSinceYield >= YIELD_BYTES) {
bytesSinceYield = 0;
await setImmediate();
}
if (!line.trim()) continue;
}
try {
const entry = oversized ? await decodeOversizedClaudeEntry(line) : decodeClaudeCliProjectEntry(line);
if (!entry) continue;
const message = parseClaudeCliHistoryEntry(entry, params.cliSessionId, lineNumber, toolNames, {
reseedMode: "recover",
reseedState
});
if (message) appendCoalescedClaudeCliToolMessage(messages, message);
} catch {}
}
const redacted = [];
for (const [index, message] of messages.entries()) {
if (index % 32 === 0) await setImmediate();
redacted.push(redactClaudeCliHistoryMessage(message));
}
return Object.freeze(redacted);
}
async function readClaudeCliSessionMessagesAsync(params) {
const source = await resolveSource(params);
if (!source) return [];
const [filePath, cacheKey] = source;
if (snapshotCache?.key !== cacheKey) snapshotCache = {
key: cacheKey,
pending: parseSnapshot(filePath, params)
};
const pending = snapshotCache.pending;
let snapshot;
try {
snapshot = await pending;
} catch {
if (snapshotCache?.pending === pending) snapshotCache = void 0;
return [];
}
const messages = [];
for (const [index, message] of snapshot.entries()) {
if (index % 32 === 0) await setImmediate();
messages.push(structuredClone(message));
}
return messages;
}
//#endregion
//#region src/gateway/cli-session-history.merge.ts
const DEDUPE_TIMESTAMP_WINDOW_MS = 3e5;
function stripTrailingCliImageMentions(text) {
const lines = text.split("\n");
let end = lines.length;
while (end > 0) {
const line = lines[end - 1]?.trim() ?? "";
if (!line.startsWith("@") || !isOpenClawCliImageCachePath(line.slice(1))) break;
end -= 1;
}
return end === lines.length ? {
text,
stripped: false
} : {
text: lines.slice(0, end).join("\n").trimEnd(),
stripped: true
};
}
function isClaudeCliImportedUserMessage(message, role) {
if (role !== "user") return false;
const meta = asOptionalRecord(asOptionalRecord(message)?.["__openclaw"]);
return normalizeOptionalString(meta?.importedFrom) === "claude-cli";
}
function extractComparableText(message, role) {
if (!message || typeof message !== "object") return { hasCliImageMentions: false };
const record = message;
const parts = [];
const text = readStringValue(record.text);
if (text !== void 0) parts.push(text);
const rawContent = record.content;
const content = readStringValue(rawContent);
if (content !== void 0) parts.push(content);
else if (Array.isArray(rawContent)) {
for (const block of rawContent) if (block && typeof block === "object" && "text" in block) {
const blockText = readStringValue(block.text);
if (blockText !== void 0) parts.push(blockText);
}
}
if (parts.length === 0) return { hasCliImageMentions: false };
const joined = parts.join("\n").trim();
if (!joined) return { hasCliImageMentions: false };
const stripResult = isClaudeCliImportedUserMessage(message, role) ? stripTrailingCliImageMentions(joined) : {
text: joined,
stripped: false
};
const normalized = stripInlineDirectiveTagsForDisplay(role === "user" ? stripInboundMetadata(stripResult.text) : stripResult.text).text.replace(/\s+/g, " ").trim();
const meta = asOptionalRecord(asOptionalRecord(message)?.["__openclaw"]);
const storedImageTurnKey = normalizeOptionalString(meta?.cliImageTurnKey);
return {
hasCliImageMentions: stripResult.stripped,
...stripResult.stripped && isClaudeCliImportedUserMessage(message, role) ? { cliImageTurnKey: storedImageTurnKey ?? readCliImageTurnContext(joined) } : {},
...normalized ? { text: normalized } : {}
};
}
function prepareComparableMessage(message, order, externalIdentityKey) {
if (!message || typeof message !== "object") return {
message,
order,
hasCliImageMentions: false
};
const record = message;
const role = readStringValue(record.role);
const comparableText = extractComparableText(message, role);
return {
message,
order,
externalIdentityKey,
hasCliImageMentions: comparableText.hasCliImageMentions,
...comparableText.cliImageTurnKey ? { cliImageTurnKey: comparableText.cliImageTurnKey } : {},
role,
text: comparableText.text,
timestamp: asFiniteNumber(record.timestamp)
};
}
function resolveImportedExternalIdentityKey(message) {
if (!message || typeof message !== "object") return;
const rawMeta = message["__openclaw"];
if (!rawMeta || typeof rawMeta !== "object") return;
const externalId = normalizeOptionalString(rawMeta.externalId);
return externalId ? JSON.stringify([
externalId,
normalizeOptionalString(rawMeta.importedFrom),
normalizeOptionalString(rawMeta.cliSessionId)
]) : void 0;
}
function addTimestampToSummary(summary, timestamp) {
if (timestamp === void 0) {
summary.hasMissingTimestamp = true;
return;
}
const bucketKey = Math.floor(timestamp / DEDUPE_TIMESTAMP_WINDOW_MS);
const bucket = summary.buckets.get(bucketKey);
if (bucket) {
bucket.min = Math.min(bucket.min, timestamp);
bucket.max = Math.max(bucket.max, timestamp);
} else summary.buckets.set(bucketKey, {
min: timestamp,
max: timestamp
});
}
function summaryHasTimestampMatch(summary, timestamp) {
if (!summary || timestamp === void 0) return false;
const bucketKey = Math.floor(timestamp / DEDUPE_TIMESTAMP_WINDOW_MS);
if (summary.buckets.has(bucketKey)) return true;
const previous = summary.buckets.get(bucketKey - 1);
if (previous && previous.max >= timestamp - DEDUPE_TIMESTAMP_WINDOW_MS) return true;
const next = summary.buckets.get(bucketKey + 1);
return next !== void 0 && next.min <= timestamp + DEDUPE_TIMESTAMP_WINDOW_MS;
}
function summaryMatchesTimestamp(summary, timestamp) {
return Boolean(summary && (timestamp === void 0 || summary.hasMissingTimestamp)) || summaryHasTimestampMatch(summary, timestamp);
}
function addRoleTextCandidate(index, entry) {
if (!entry.role || !entry.text) return;
let byText = index.get(entry.role);
if (!byText) {
byText = /* @__PURE__ */ new Map();
index.set(entry.role, byText);
}
let summary = byText.get(entry.text);
if (!summary) {
summary = {
hasMissingTimestamp: false,
buckets: /* @__PURE__ */ new Map()
};
byText.set(entry.text, summary);
}
addTimestampToSummary(summary, entry.timestamp);
}
function hasRoleTextCandidate(index, entry) {
if (!entry.role || !entry.text) return false;
return summaryMatchesTimestamp(index.get(entry.role)?.get(entry.text), entry.timestamp);
}
function hasLocalImageMediaFacts(entry) {
if (entry.role !== "user") return false;
const message = asOptionalRecord(entry.message);
return message ? (readPersistedMediaFacts(message) ?? []).some(isImageMediaFact) : false;
}
function compareHistoryMessages(a, b) {
if (a.timestamp !== void 0 && b.timestamp !== void 0 && a.timestamp !== b.timestamp) return a.timestamp - b.timestamp;
return a.order - b.order;
}
/** Merges imported CLI transcript messages into local history without duplicating overlaps. */
function mergeImportedChatHistoryMessages(params) {
if (params.importedMessages.length === 0) return params.localMessages;
const merged = params.localMessages.map((message, order) => prepareComparableMessage(message, order, resolveImportedExternalIdentityKey(message)));
const exactExternalIdentityIndex = /* @__PURE__ */ new Set();
const allMessageRoleTextIndex = /* @__PURE__ */ new Map();
const identitylessRoleTextIndex = /* @__PURE__ */ new Map();
const localImageMediaCounts = /* @__PURE__ */ new Map();
const indexEntry = (entry) => {
if (entry.externalIdentityKey) exactExternalIdentityIndex.add(entry.externalIdentityKey);
else addRoleTextCandidate(identitylessRoleTextIndex, entry);
addRoleTextCandidate(allMessageRoleTextIndex, entry);
};
for (const entry of merged) {
indexEntry(entry);
if (!hasLocalImageMediaFacts(entry)) continue;
const localMeta = asOptionalRecord(asOptionalRecord(entry.message)?.["__openclaw"]);
const localEntryId = normalizeOptionalString(localMeta?.id);
const turnKey = localEntryId ? hashCliImageTurnEntryId(localEntryId) : entry.cliImageTurnKey;
if (turnKey) localImageMediaCounts.set(turnKey, (localImageMediaCounts.get(turnKey) ?? 0) + 1);
}
let nextOrder = merged.length;
for (const message of params.importedMessages) {
const externalIdentityKey = resolveImportedExternalIdentityKey(message);
if (externalIdentityKey && exactExternalIdentityIndex.has(externalIdentityKey)) continue;
const imported = prepareComparableMessage(message, nextOrder, externalIdentityKey);
const turnKey = imported.hasCliImageMentions ? imported.cliImageTurnKey : void 0;
const matches = turnKey ? localImageMediaCounts.get(turnKey) : void 0;
if (turnKey && matches) {
localImageMediaCounts.set(turnKey, matches - 1);
continue;
}
const duplicate = imported.externalIdentityKey ? hasRoleTextCandidate(identitylessRoleTextIndex, imported) : hasRoleTextCandidate(allMessageRoleTextIndex, imported);
if (!imported.hasCliImageMentions && duplicate) continue;
merged.push(imported);
indexEntry(imported);
nextOrder += 1;
}
merged.sort(compareHistoryMessages);
return merged.map((entry) => entry.message);
}
//#endregion
//#region src/gateway/cli-session-history.ts
const ANTHROPIC_PROVIDER = "anthropic";
function resolveEligibleCliSessionBinding(params) {
const binding = getCliSessionBinding(params.entry, CLAUDE_CLI_PROVIDER);
const provider = normalizeProviderId(params.provider ?? "");
const eligible = !provider || params.localMessages.length === 0 || provider === "claude-cli" || provider === ANTHROPIC_PROVIDER;
return binding?.sessionId && eligible ? binding : void 0;
}
/** Resolves chat history plus whether a bound external transcript was actually incorporated. */
function resolveChatHistoryWithCliSessionImports(params) {
const binding = resolveEligibleCliSessionBinding(params);
if (!binding) return {
messages: params.localMessages,
imported: false
};
const importedMessages = params.preparedImportedMessages ?? readClaudeCliSessionMessages({
cliSessionId: binding.sessionId,
homeDir: params.homeDir,
localSessionId: params.entry?.sessionId,
reseedReceipt: binding.reseedReceipt
});
if (importedMessages.length === 0) return {
messages: params.localMessages,
imported: false
};
const messages = mergeImportedChatHistoryMessages({
localMessages: params.localMessages,
importedMessages
});
return messages.length > params.localMessages.length ? {
messages,
imported: true
} : {
messages: params.localMessages,
imported: false
};
}
/** Acquires one request-local redacted view of the process-owned external snapshot. */
async function readChatHistoryCliSessionImportSnapshot(params) {
const binding = resolveEligibleCliSessionBinding(params);
return binding?.sessionId ? await readClaudeCliSessionMessagesAsync({
cliSessionId: binding.sessionId,
homeDir: params.homeDir,
localSessionId: params.entry?.sessionId,
reseedReceipt: binding.reseedReceipt
}) : [];
}
//#endregion
export { resolveChatHistoryWithCliSessionImports as n, readChatHistoryCliSessionImportSnapshot as t };