Skip to content

Commit d122a34

Browse files
committed
fix(gateway): preserve oversized transcript tree leaves
1 parent 1a5ad61 commit d122a34

2 files changed

Lines changed: 76 additions & 9 deletions

File tree

src/gateway/session-utils.fs.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,38 @@ describe("oversized transcript line guards", () => {
17041704
expect(serialized).toContain("after oversized");
17051705
});
17061706

1707+
test("readRecentSessionMessagesAsync keeps oversized active-tree leaves", async () => {
1708+
const sessionId = "test-oversized-tree-tail";
1709+
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
1710+
const oversizedContent = "z".repeat(300 * 1024);
1711+
const lines = [
1712+
JSON.stringify({ type: "session", version: 3, id: sessionId }),
1713+
JSON.stringify({
1714+
type: "message",
1715+
id: "root",
1716+
parentId: null,
1717+
message: { role: "user", content: "root" },
1718+
}),
1719+
JSON.stringify({
1720+
type: "message",
1721+
id: "oversized-leaf",
1722+
parentId: "root",
1723+
message: { role: "assistant", content: oversizedContent },
1724+
}),
1725+
];
1726+
fs.writeFileSync(transcriptPath, `${lines.join("\n")}\n`, "utf-8");
1727+
1728+
const out = await readRecentSessionMessagesAsync(sessionId, storePath, undefined, {
1729+
maxMessages: 10,
1730+
});
1731+
1732+
const serialized = JSON.stringify(out);
1733+
expect(serialized).toContain("root");
1734+
expect(serialized).toContain("oversized-leaf");
1735+
expect(serialized).not.toContain(oversizedContent);
1736+
expect(serialized).toContain("[chat.history omitted: message too large]");
1737+
});
1738+
17071739
test("readRecentSessionUsageFromTranscriptAsync skips oversized lines", async () => {
17081740
const sessionId = "test-oversized-usage";
17091741
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);

src/gateway/session-utils.fs.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -267,22 +267,57 @@ async function readRecentTranscriptTailLinesAsync(
267267
}
268268

269269
const MAX_TRANSCRIPT_PARSE_LINE_BYTES = 256 * 1024;
270+
const OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS = 64 * 1024;
270271
const TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER = "[chat.history omitted: message too large]";
271272

272273
function isOversizedTranscriptLine(line: string): boolean {
273274
return Buffer.byteLength(line, "utf8") > MAX_TRANSCRIPT_PARSE_LINE_BYTES;
274275
}
275276

276-
function buildOversizedTranscriptRecord(): TailTranscriptRecord {
277-
return {
278-
record: {
279-
message: {
280-
role: "assistant",
281-
content: [{ type: "text", text: TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER }],
282-
__openclaw: { truncated: true, reason: "oversized" },
283-
},
277+
function extractJsonStringFieldPrefix(prefix: string, field: string): string | undefined {
278+
const match = new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`).exec(prefix);
279+
if (!match) {
280+
return undefined;
281+
}
282+
try {
283+
const decoded = JSON.parse(`"${match[1]}"`) as unknown;
284+
return normalizeTailEntryString(decoded);
285+
} catch {
286+
return undefined;
287+
}
288+
}
289+
290+
function extractJsonNullableStringFieldPrefix(
291+
prefix: string,
292+
field: string,
293+
): string | null | undefined {
294+
if (new RegExp(`"${field}"\\s*:\\s*null`).test(prefix)) {
295+
return null;
296+
}
297+
return extractJsonStringFieldPrefix(prefix, field);
298+
}
299+
300+
function buildOversizedTranscriptRecord(line: string): TailTranscriptRecord {
301+
const prefix = line.slice(0, OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS);
302+
const id = extractJsonStringFieldPrefix(prefix, "id");
303+
const parentId = extractJsonNullableStringFieldPrefix(prefix, "parentId");
304+
const type = extractJsonStringFieldPrefix(prefix, "type");
305+
const role = extractJsonStringFieldPrefix(prefix, "role") ?? "assistant";
306+
const record: Record<string, unknown> = {
307+
...(type ? { type } : {}),
308+
...(id ? { id } : {}),
309+
...(parentId !== undefined ? { parentId } : {}),
310+
message: {
311+
role,
312+
content: [{ type: "text", text: TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER }],
313+
__openclaw: { truncated: true, reason: "oversized" },
284314
},
285315
};
316+
return {
317+
...(id ? { id } : {}),
318+
...(parentId !== undefined ? { parentId } : {}),
319+
record,
320+
};
286321
}
287322

288323
function normalizeTailEntryString(value: unknown): string | undefined {
@@ -291,7 +326,7 @@ function normalizeTailEntryString(value: unknown): string | undefined {
291326

292327
function parseTailTranscriptRecord(line: string): TailTranscriptRecord | null {
293328
if (isOversizedTranscriptLine(line)) {
294-
return buildOversizedTranscriptRecord();
329+
return buildOversizedTranscriptRecord(line);
295330
}
296331
try {
297332
const parsed = JSON.parse(line) as unknown;

0 commit comments

Comments
 (0)