diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index d5d04f02dd9..53f5013e0ca 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -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`); diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index ddb0a026df7..5db2edba50a 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -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 = { + ...(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;