openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
679 lines (678 loc) • 28.2 kB
JavaScript
import { g as readStringValue, l as normalizeOptionalString } from "./string-coerce-6kR-86XC.mjs";
import { s as asFiniteNumber } from "./number-coercion-Dq1l5muL.mjs";
import { a as asOptionalRecord, c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { n as createCpuTrackedWorker } from "./worker-cpu-EKuz4_X-.mjs";
import { r as normalizeProviderId } from "./provider-id-BIyeXQag.mjs";
import { n as getCliSessionBinding, r as normalizeCliSessionReseedReceipt } from "./cli-session-binding-Xt6nY3mk.mjs";
import { d as readPersistedMediaFacts, o as isImageMediaFact } from "./media-facts-CYny4oQ-.mjs";
import { u as stripInlineDirectiveTagsForDisplay } from "./directive-tags-BDwXb2d0.mjs";
import { i as stripInboundMetadata } from "./strip-inbound-meta-CRySRUKv.mjs";
import { f as stripCliSessionDriftNote } from "./cli-session-BPTj0KNq.mjs";
import { r as isOpenClawCliImageCachePath } from "./images.media-refs-yK-8_pTF.mjs";
import { n as hashCliImageTurnEntryId, r as readCliImageTurnContext } from "./cli-image-turn-correlation-BVXO9f3I.mjs";
import { a as parseClaudeCliHistoryEntry, c as redactClaudeCliHistoryMessage, d as resolveClaudeCliSessionFilePathAsync, i as decodeClaudeCliProjectEntry, n as appendCoalescedClaudeCliToolMessage, r as createClaudeReseedImportState, s as readClaudeCliSessionMessages, t as CLAUDE_CLI_PROVIDER } from "./cli-session-history.claude-DmA4TGQZ.mjs";
import fs from "node:fs";
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 } = require("node:worker_threads");
const boundedString = (value, max) =>
typeof value === "string" && value.length <= max ? value : undefined;
parentPort.on("message", (line) => {
try {
const entry = JSON.parse(line);
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;
const pendingSnapshots = /* @__PURE__ */ new Map();
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(worker, line) {
return await new Promise((resolve) => {
let settled = false;
const finish = (value) => {
if (settled) return;
settled = true;
worker.off("message", finish);
worker.off("error", fail);
worker.off("exit", fail);
resolve(normalizeOversizedEntry(value));
};
const fail = () => finish(null);
worker.once("message", finish);
worker.once("error", fail);
worker.once("exit", fail);
try {
worker.postMessage(line, []);
} catch {
fail();
}
});
}
function fingerprint(stats) {
return [
stats.dev,
stats.ino,
stats.size,
stats.mtimeMs,
stats.ctimeMs
].join(":");
}
async function resolveSource(params) {
const candidate = await resolveClaudeCliSessionFilePathAsync(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;
let worker;
try {
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 {
let entry;
if (oversized) {
if (!worker || worker.threadId === -1) {
worker = createCpuTrackedWorker(OVERSIZED_ENTRY_WORKER_SOURCE, { eval: true });
worker.on("error", () => {});
}
entry = await decodeOversizedClaudeEntry(worker, line);
} else entry = decodeClaudeCliProjectEntry(line);
if (!entry) continue;
const message = parseClaudeCliHistoryEntry(entry, params.cliSessionId, lineNumber, toolNames, {
reseedMode: "recover",
reseedState
});
if (message) appendCoalescedClaudeCliToolMessage(messages, message);
} catch {}
}
} finally {
await worker?.terminate();
}
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) {
const pending = pendingSnapshots.get(cacheKey) ?? parseSnapshot(filePath, params);
pendingSnapshots.set(cacheKey, pending);
snapshotCache = {
key: cacheKey,
pending
};
}
const pending = snapshotCache.pending;
let snapshot;
try {
snapshot = await pending;
} catch {
if (snapshotCache?.pending === pending) snapshotCache = void 0;
return [];
} finally {
if (pendingSnapshots.get(cacheKey) === pending) pendingSnapshots.delete(cacheKey);
}
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 rawText = parts.join("\n");
const joined = rawText.trim();
if (!joined) return { hasCliImageMentions: false };
const isClaudeImport = isClaudeCliImportedUserMessage(message, role);
const stripResult = isClaudeImport ? stripTrailingCliImageMentions(joined) : {
text: joined,
stripped: false
};
const normalizeText = (value) => {
return stripInlineDirectiveTagsForDisplay(role === "user" ? stripInboundMetadata(value) : value).text.replace(/\s+/g, " ").trim();
};
const normalized = normalizeText(stripResult.text);
const withoutDriftNote = isClaudeImport ? stripCliSessionDriftNote(rawText) : rawText;
const driftNoteText = withoutDriftNote !== rawText ? normalizeText(stripTrailingCliImageMentions(withoutDriftNote.trim()).text) : void 0;
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 } : {},
...driftNoteText ? { driftNoteText } : {}
};
}
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,
driftNoteText: comparableText.driftNoteText,
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, entry) {
if (entry.timestamp === void 0) {
summary.missingTimestamps.push(entry);
return;
}
summary.timestampedByOrder.push(entry);
summary.timestampRoot = insertTimestampCandidate(summary.timestampRoot, entry);
}
function compareTimestampCandidates(left, right) {
return (left.timestamp ?? 0) - (right.timestamp ?? 0) || left.order - right.order;
}
function timestampCandidateHeight(node) {
return node?.height ?? 0;
}
function updateTimestampCandidate(node) {
node.height = Math.max(timestampCandidateHeight(node.left), timestampCandidateHeight(node.right)) + 1;
node.minOrder = Math.min(node.entry.order, node.left?.minOrder ?? Number.POSITIVE_INFINITY, node.right?.minOrder ?? Number.POSITIVE_INFINITY);
node.maxOrder = Math.max(node.entry.order, node.left?.maxOrder ?? Number.NEGATIVE_INFINITY, node.right?.maxOrder ?? Number.NEGATIVE_INFINITY);
}
function rotateTimestampCandidateLeft(root) {
const next = root.right;
if (!next) return root;
root.right = next.left;
next.left = root;
updateTimestampCandidate(root);
updateTimestampCandidate(next);
return next;
}
function rotateTimestampCandidateRight(root) {
const next = root.left;
if (!next) return root;
root.left = next.right;
next.right = root;
updateTimestampCandidate(root);
updateTimestampCandidate(next);
return next;
}
function balanceTimestampCandidate(root) {
updateTimestampCandidate(root);
const balance = timestampCandidateHeight(root.left) - timestampCandidateHeight(root.right);
if (balance > 1) {
if (root.left && timestampCandidateHeight(root.left.left) < timestampCandidateHeight(root.left.right)) root.left = rotateTimestampCandidateLeft(root.left);
return rotateTimestampCandidateRight(root);
}
if (balance < -1) {
if (root.right && timestampCandidateHeight(root.right.right) < timestampCandidateHeight(root.right.left)) root.right = rotateTimestampCandidateRight(root.right);
return rotateTimestampCandidateLeft(root);
}
return root;
}
function insertTimestampCandidate(root, entry) {
if (!root) return {
entry,
height: 1,
minOrder: entry.order,
maxOrder: entry.order
};
if (compareTimestampCandidates(entry, root.entry) < 0) root.left = insertTimestampCandidate(root.left, entry);
else root.right = insertTimestampCandidate(root.right, entry);
return balanceTimestampCandidate(root);
}
function removeTimestampCandidate(root, entry) {
if (!root) return;
const comparison = compareTimestampCandidates(entry, root.entry);
if (comparison < 0) root.left = removeTimestampCandidate(root.left, entry);
else if (comparison > 0) root.right = removeTimestampCandidate(root.right, entry);
else if (!root.left || !root.right) return root.left ?? root.right;
else {
let successor = root.right;
while (successor.left) successor = successor.left;
root.entry = successor.entry;
root.right = removeTimestampCandidate(root.right, successor.entry);
}
return balanceTimestampCandidate(root);
}
function findFirstTimestampCandidateInRange(root, minimumTimestamp, maximumTimestamp, minimumOrder, maximumOrder = Number.POSITIVE_INFINITY) {
if (!root || root.maxOrder < minimumOrder || root.minOrder >= maximumOrder) return;
const rootTimestamp = root.entry.timestamp ?? 0;
let best = rootTimestamp >= minimumTimestamp && rootTimestamp <= maximumTimestamp && root.entry.order >= minimumOrder ? root.entry : void 0;
const left = rootTimestamp >= minimumTimestamp ? root.left : void 0;
const right = rootTimestamp <= maximumTimestamp ? root.right : void 0;
const [first, second] = (left?.minOrder ?? Number.POSITIVE_INFINITY) <= (right?.minOrder ?? Number.POSITIVE_INFINITY) ? [left, right] : [right, left];
for (const child of [first, second]) {
const candidate = findFirstTimestampCandidateInRange(child, minimumTimestamp, maximumTimestamp, minimumOrder, best?.order ?? maximumOrder);
if (candidate && (!best || candidate.order < best.order)) best = candidate;
}
return best;
}
function findMinimumOrderCursor(entries, startCursor, minimumOrder) {
let cursor = startCursor;
let end = entries.length;
while (cursor < end) {
const middle = Math.floor((cursor + end) / 2);
const candidate = entries[middle];
if (candidate && candidate.order < minimumOrder) cursor = middle + 1;
else end = middle;
}
return cursor;
}
function findTimestampMatch(summary, timestamp, consumed, minimumOrder) {
if (!summary) return;
while (summary.missingTimestampCursor < summary.missingTimestamps.length) {
const candidate = summary.missingTimestamps[summary.missingTimestampCursor];
if (!candidate || !consumed.has(candidate)) break;
summary.missingTimestampCursor += 1;
}
while (summary.timestampedOrderCursor < summary.timestampedByOrder.length) {
const candidate = summary.timestampedByOrder[summary.timestampedOrderCursor];
if (!candidate || !consumed.has(candidate)) break;
summary.timestampedOrderCursor += 1;
}
if (timestamp === void 0) {
let missingCursor = findMinimumOrderCursor(summary.missingTimestamps, summary.missingTimestampCursor, minimumOrder);
let timestampedCursor = findMinimumOrderCursor(summary.timestampedByOrder, summary.timestampedOrderCursor, minimumOrder);
while (missingCursor < summary.missingTimestamps.length) {
const candidate = summary.missingTimestamps[missingCursor];
if (candidate && candidate.order >= minimumOrder && !consumed.has(candidate)) break;
missingCursor += 1;
}
while (timestampedCursor < summary.timestampedByOrder.length) {
const candidate = summary.timestampedByOrder[timestampedCursor];
if (candidate && candidate.order >= minimumOrder && !consumed.has(candidate)) break;
timestampedCursor += 1;
}
const missing = summary.missingTimestamps[missingCursor];
const timestamped = summary.timestampedByOrder[timestampedCursor];
const candidate = missing && (!timestamped || missing.order < timestamped.order) ? missing : timestamped;
if (!candidate) return;
summary.missingTimestampCursor = missingCursor + (candidate === missing ? 1 : 0);
summary.timestampedOrderCursor = timestampedCursor + (candidate === timestamped ? 1 : 0);
return candidate;
}
let timestamped = findFirstTimestampCandidateInRange(summary.timestampRoot, timestamp - DEDUPE_TIMESTAMP_WINDOW_MS, timestamp + DEDUPE_TIMESTAMP_WINDOW_MS, minimumOrder);
while (timestamped && consumed.has(timestamped)) {
summary.timestampRoot = removeTimestampCandidate(summary.timestampRoot, timestamped);
timestamped = findFirstTimestampCandidateInRange(summary.timestampRoot, timestamp - DEDUPE_TIMESTAMP_WINDOW_MS, timestamp + DEDUPE_TIMESTAMP_WINDOW_MS, minimumOrder);
}
if (timestamped) {
while (summary.timestampedOrderCursor < summary.timestampedByOrder.length && (summary.timestampedByOrder[summary.timestampedOrderCursor]?.order ?? Number.POSITIVE_INFINITY) <= timestamped.order) {
const skipped = summary.timestampedByOrder[summary.timestampedOrderCursor];
if (skipped) summary.timestampRoot = removeTimestampCandidate(summary.timestampRoot, skipped);
summary.timestampedOrderCursor += 1;
}
return timestamped;
}
let missingCursor = findMinimumOrderCursor(summary.missingTimestamps, summary.missingTimestampCursor, minimumOrder);
while (missingCursor < summary.missingTimestamps.length) {
const candidate = summary.missingTimestamps[missingCursor];
if (candidate && candidate.order >= minimumOrder && !consumed.has(candidate)) {
summary.missingTimestampCursor = missingCursor + 1;
return candidate;
}
missingCursor += 1;
}
}
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 = {
missingTimestamps: [],
missingTimestampCursor: 0,
timestampedByOrder: [],
timestampedOrderCursor: 0
};
byText.set(entry.text, summary);
}
addTimestampToSummary(summary, entry);
}
function hasLocalImageMediaFacts(entry) {
if (entry.role !== "user") return false;
const message = asOptionalRecord(entry.message);
return message ? (readPersistedMediaFacts(message) ?? []).some(isImageMediaFact) : false;
}
function projectImportedIdentity(localMessage, importedMessage) {
const local = asOptionalRecord(localMessage);
const imported = asOptionalRecord(importedMessage);
const importedMeta = asOptionalRecord(imported?.["__openclaw"]);
if (!local || !importedMeta) return localMessage;
const localMeta = asOptionalRecord(local["__openclaw"]);
const nextMeta = localMeta ? { ...localMeta } : {};
let changed = false;
for (const field of [
"importedFrom",
"externalId",
"cliSessionId"
]) {
const value = normalizeOptionalString(importedMeta[field]);
if (value && nextMeta[field] === void 0) {
nextMeta[field] = value;
changed = true;
}
}
return changed ? {
...local,
__openclaw: nextMeta
} : localMessage;
}
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 Map();
const allMessageRoleTextIndex = /* @__PURE__ */ new Map();
const identitylessRoleTextIndex = /* @__PURE__ */ new Map();
const roleTextMinimumOrder = /* @__PURE__ */ new Map();
const localImageMediaCandidates = /* @__PURE__ */ new Map();
const consumedLocalCandidates = /* @__PURE__ */ new Set();
const advanceRoleTextMinimumOrder = (entry, matched) => {
if (!entry.role) return;
const matchedText = matched.text === entry.text ? entry.text : entry.driftNoteText;
for (const text of [entry.text, matchedText]) {
if (!text) continue;
let byText = roleTextMinimumOrder.get(entry.role);
if (!byText) {
byText = /* @__PURE__ */ new Map();
roleTextMinimumOrder.set(entry.role, byText);
}
byText.set(text, Math.max(byText.get(text) ?? 0, matched.order + 1));
}
};
const indexEntry = (entry) => {
if (entry.externalIdentityKey) exactExternalIdentityIndex.set(entry.externalIdentityKey, entry);
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) {
const candidates = localImageMediaCandidates.get(turnKey) ?? {
entries: [],
cursor: 0
};
candidates.entries.push(entry);
localImageMediaCandidates.set(turnKey, candidates);
}
}
for (const message of params.importedMessages) {
const externalIdentityKey = resolveImportedExternalIdentityKey(message);
const exactIdentityMatch = externalIdentityKey ? exactExternalIdentityIndex.get(externalIdentityKey) : void 0;
if (exactIdentityMatch) consumedLocalCandidates.add(exactIdentityMatch);
}
let changed = false;
let expanded = false;
let nextOrder = merged.length;
for (const message of params.importedMessages) {
const externalIdentityKey = resolveImportedExternalIdentityKey(message);
const imported = prepareComparableMessage(message, nextOrder, externalIdentityKey);
if (externalIdentityKey) {
const exactIdentityMatch = exactExternalIdentityIndex.get(externalIdentityKey);
if (exactIdentityMatch) {
consumedLocalCandidates.add(exactIdentityMatch);
advanceRoleTextMinimumOrder(imported, exactIdentityMatch);
continue;
}
}
const turnKey = imported.hasCliImageMentions ? imported.cliImageTurnKey : void 0;
const imageCandidates = turnKey ? localImageMediaCandidates.get(turnKey) : void 0;
let duplicate;
if (imageCandidates) {
duplicate = imageCandidates.entries[imageCandidates.cursor];
while (duplicate && consumedLocalCandidates.has(duplicate)) {
imageCandidates.cursor += 1;
duplicate = imageCandidates.entries[imageCandidates.cursor];
}
if (duplicate) imageCandidates.cursor += 1;
}
if (!duplicate && !imported.hasCliImageMentions) {
const index = imported.externalIdentityKey ? identitylessRoleTextIndex : allMessageRoleTextIndex;
const byText = imported.role ? roleTextMinimumOrder.get(imported.role) : void 0;
const importedMinimumOrder = imported.text ? byText?.get(imported.text) ?? 0 : 0;
for (const text of [imported.text, imported.driftNoteText]) {
if (!imported.role || !text) continue;
const minimumOrder = Math.max(importedMinimumOrder, byText?.get(text) ?? 0);
duplicate = findTimestampMatch(index.get(imported.role)?.get(text), imported.timestamp, consumedLocalCandidates, minimumOrder);
if (duplicate) break;
}
}
if (duplicate) {
const projected = projectImportedIdentity(duplicate.message, imported.message);
if (projected !== duplicate.message) {
duplicate.message = projected;
duplicate.externalIdentityKey = resolveImportedExternalIdentityKey(projected);
if (duplicate.externalIdentityKey) exactExternalIdentityIndex.set(duplicate.externalIdentityKey, duplicate);
changed = true;
}
consumedLocalCandidates.add(duplicate);
advanceRoleTextMinimumOrder(imported, duplicate);
continue;
}
merged.push(imported);
indexEntry(imported);
consumedLocalCandidates.add(imported);
nextOrder += 1;
changed = true;
expanded = true;
}
if (!changed) return params.localMessages;
if (!expanded) return merged.map((entry) => entry.message);
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,
expanded: 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,
expanded: false
};
const messages = mergeImportedChatHistoryMessages({
localMessages: params.localMessages,
importedMessages
});
return {
messages,
imported: messages !== params.localMessages,
expanded: messages.length > params.localMessages.length
};
}
/** 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 };