openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
177 lines (176 loc) • 5.73 kB
JavaScript
import { n as estimateStringChars } from "./cjk-chars-6ld30jSx.mjs";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { t as hashText } from "./hash-BOTtT1fO.mjs";
//#region packages/memory-host-sdk/src/host/markdown-chunks.ts
const MEMORY_CHUNKING_VERSION = 5;
function curatedMarkdownEntryKind(line) {
return line.startsWith("- ") ? "entry" : /^#{1,6}(?:\s|$)/u.test(line) ? "section" : void 0;
}
function splitCuratedMarkdownEntries(content) {
const lines = content.split("\n");
const entries = [];
let startIndex = 0;
let kind = lines[0]?.startsWith("- ") ? "entry" : "section";
const flush = (endIndex) => {
if (endIndex < startIndex) return;
entries.push({
startLine: startIndex + 1,
endLine: endIndex + 1,
text: lines.slice(startIndex, endIndex + 1).join("\n"),
kind
});
};
for (let index = 1; index < lines.length; index += 1) {
const nextKind = curatedMarkdownEntryKind(lines[index] ?? "");
if (!nextKind) continue;
flush(index - 1);
startIndex = index;
kind = nextKind;
}
flush(lines.length - 1);
return entries;
}
/** Takes the trailing slice of text within the weighted char budget, without splitting surrogate pairs. */
function takeTailByEstimatedChars(text, budget) {
let acc = 0;
let start = text.length;
while (start > 0) {
const previous = start - ((text.codePointAt(start - 2) ?? 0) > 65535 ? 2 : 1);
acc += estimateStringChars(text.slice(previous, start));
if (!(acc <= budget)) break;
start = previous;
}
return text.slice(start);
}
function chunkMarkdown(content, chunking) {
const lines = content.split("\n");
const maxChars = Math.max(32, chunking.tokens * 4);
const overlapChars = Math.max(0, chunking.overlap * 4);
const chunks = [];
let current = [];
let currentChars = 0;
let entryStartLine;
let entryFirstChunk = 0;
const flush = () => {
const firstEntry = current[0];
const lastEntry = current[current.length - 1];
if (!firstEntry || !lastEntry) return;
const text = current.map((entry) => entry.line).join("\n");
const startLine = firstEntry.lineNo;
const endLine = lastEntry.lineNo;
chunks.push({
startLine,
endLine,
text,
hash: hashText(text),
embeddingInput: { text }
});
};
const carryOverlap = (window) => {
if (window <= 0 || current.length === 0) {
current = [];
currentChars = 0;
return;
}
let acc = 0;
const kept = [];
for (let i = current.length - 1; i >= 0; i -= 1) {
const entry = current[i];
if (!entry) continue;
const entrySize = estimateStringChars(entry.line) + 1;
const remaining = window - acc;
if (entrySize > remaining) {
const tail = kept.length === 0 ? takeTailByEstimatedChars(entry.line, remaining - 1) : "";
if (tail.length > 0) {
kept.unshift({
line: tail,
lineNo: entry.lineNo
});
acc += estimateStringChars(tail) + 1;
}
break;
}
acc += entrySize;
kept.unshift(entry);
if (acc >= window) break;
}
current = kept;
currentChars = acc;
};
const appendSegment = (segment, lineNo, chars) => {
const lineSize = chars + 1;
if (currentChars + lineSize > maxChars && current.length > 0) {
flush();
carryOverlap(Math.min(overlapChars, Math.max(0, maxChars - lineSize)));
}
current.push({
line: segment,
lineNo
});
currentChars += lineSize;
};
const finishEntry = (entryEndLine) => {
if (entryStartLine === void 0) return;
for (const chunk of chunks.slice(entryFirstChunk)) {
chunk.entryStartLine = entryStartLine;
chunk.entryEndLine = entryEndLine;
}
};
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i] ?? "";
const lineNo = i + 1;
const entryKind = chunking.perEntry ? curatedMarkdownEntryKind(line) ?? (i === 0 ? "section" : void 0) : void 0;
if (entryKind) {
if (current.length > 0) flush();
finishEntry(lineNo - 1);
current = [];
currentChars = 0;
entryStartLine = entryKind === "entry" ? lineNo : void 0;
entryFirstChunk = chunks.length;
}
if (line.length === 0) appendSegment("", lineNo, 0);
else for (let start = 0; start < line.length;) {
const coarse = truncateUtf16Safe(line.slice(start), maxChars);
const coarseChars = estimateStringChars(coarse);
if (coarseChars > maxChars) {
let partStart = 0;
let partEnd = 0;
let partChars = 0;
for (const character of coarse) {
const chars = estimateStringChars(character);
if (partChars + chars > maxChars) {
appendSegment(coarse.slice(partStart, partEnd), lineNo, partChars);
partStart = partEnd;
partChars = 0;
}
partEnd += character.length;
partChars += chars;
}
appendSegment(coarse.slice(partStart), lineNo, partChars);
} else appendSegment(coarse, lineNo, coarseChars);
start += coarse.length;
}
}
flush();
finishEntry(lines.length);
return chunks;
}
/**
* Remap chunk startLine/endLine from content-relative positions to original
* source file positions using a lineMap. Each entry in lineMap gives the
* 1-indexed source line for the corresponding 0-indexed content line.
*
* This is used for session JSONL files where buildSessionEntry() flattens
* messages into a plain-text string before chunking. Without remapping the
* stored line numbers would reference positions in the flattened text rather
* than the original JSONL file.
*/
function remapChunkLines(chunks, lineMap) {
if (!lineMap || lineMap.length === 0) return;
for (const chunk of chunks) {
chunk.startLine = lineMap[chunk.startLine - 1] ?? chunk.startLine;
chunk.endLine = lineMap[chunk.endLine - 1] ?? chunk.endLine;
}
}
//#endregion
export { splitCuratedMarkdownEntries as i, chunkMarkdown as n, remapChunkLines as r, MEMORY_CHUNKING_VERSION as t };