fix(gateway): preserve oversized transcript tree leaves

This commit is contained in:
Vincent Koc
2026-05-02 21:27:40 -07:00
parent 1a5ad6104d
commit d122a3492d
2 changed files with 76 additions and 9 deletions

View File

@@ -1704,6 +1704,38 @@ describe("oversized transcript line guards", () => {
expect(serialized).toContain("after oversized");
});
test("readRecentSessionMessagesAsync keeps oversized active-tree leaves", async () => {
const sessionId = "test-oversized-tree-tail";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const oversizedContent = "z".repeat(300 * 1024);
const lines = [
JSON.stringify({ type: "session", version: 3, id: sessionId }),
JSON.stringify({
type: "message",
id: "root",
parentId: null,
message: { role: "user", content: "root" },
}),
JSON.stringify({
type: "message",
id: "oversized-leaf",
parentId: "root",
message: { role: "assistant", content: oversizedContent },
}),
];
fs.writeFileSync(transcriptPath, `${lines.join("\n")}\n`, "utf-8");
const out = await readRecentSessionMessagesAsync(sessionId, storePath, undefined, {
maxMessages: 10,
});
const serialized = JSON.stringify(out);
expect(serialized).toContain("root");
expect(serialized).toContain("oversized-leaf");
expect(serialized).not.toContain(oversizedContent);
expect(serialized).toContain("[chat.history omitted: message too large]");
});
test("readRecentSessionUsageFromTranscriptAsync skips oversized lines", async () => {
const sessionId = "test-oversized-usage";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);

View File

@@ -267,22 +267,57 @@ async function readRecentTranscriptTailLinesAsync(
}
const MAX_TRANSCRIPT_PARSE_LINE_BYTES = 256 * 1024;
const OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS = 64 * 1024;
const TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER = "[chat.history omitted: message too large]";
function isOversizedTranscriptLine(line: string): boolean {
return Buffer.byteLength(line, "utf8") > MAX_TRANSCRIPT_PARSE_LINE_BYTES;
}
function buildOversizedTranscriptRecord(): TailTranscriptRecord {
return {
record: {
message: {
role: "assistant",
content: [{ type: "text", text: TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER }],
__openclaw: { truncated: true, reason: "oversized" },
},
function extractJsonStringFieldPrefix(prefix: string, field: string): string | undefined {
const match = new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`).exec(prefix);
if (!match) {
return undefined;
}
try {
const decoded = JSON.parse(`"${match[1]}"`) as unknown;
return normalizeTailEntryString(decoded);
} catch {
return undefined;
}
}
function extractJsonNullableStringFieldPrefix(
prefix: string,
field: string,
): string | null | undefined {
if (new RegExp(`"${field}"\\s*:\\s*null`).test(prefix)) {
return null;
}
return extractJsonStringFieldPrefix(prefix, field);
}
function buildOversizedTranscriptRecord(line: string): TailTranscriptRecord {
const prefix = line.slice(0, OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS);
const id = extractJsonStringFieldPrefix(prefix, "id");
const parentId = extractJsonNullableStringFieldPrefix(prefix, "parentId");
const type = extractJsonStringFieldPrefix(prefix, "type");
const role = extractJsonStringFieldPrefix(prefix, "role") ?? "assistant";
const record: Record<string, unknown> = {
...(type ? { type } : {}),
...(id ? { id } : {}),
...(parentId !== undefined ? { parentId } : {}),
message: {
role,
content: [{ type: "text", text: TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER }],
__openclaw: { truncated: true, reason: "oversized" },
},
};
return {
...(id ? { id } : {}),
...(parentId !== undefined ? { parentId } : {}),
record,
};
}
function normalizeTailEntryString(value: unknown): string | undefined {
@@ -291,7 +326,7 @@ function normalizeTailEntryString(value: unknown): string | undefined {
function parseTailTranscriptRecord(line: string): TailTranscriptRecord | null {
if (isOversizedTranscriptLine(line)) {
return buildOversizedTranscriptRecord();
return buildOversizedTranscriptRecord(line);
}
try {
const parsed = JSON.parse(line) as unknown;