From 003ce4e248185b6493d7608773722c31dfae1ec4 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:36:26 +0000 Subject: [PATCH] fix(claude): scan large transcript records once Retain unfinished record chunks and assemble each record once instead of copying and rescanning a growing buffer on every backward read. Cover import of a large record after the last explicit rename. --- .../claude/agent.import-titles.test.ts | 15 +++++ .../server/agent/providers/claude/agent.ts | 59 ++++++------------- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude/agent.import-titles.test.ts b/packages/server/src/server/agent/providers/claude/agent.import-titles.test.ts index 0e30f39dae..a826645d4c 100644 --- a/packages/server/src/server/agent/providers/claude/agent.import-titles.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.import-titles.test.ts @@ -282,4 +282,19 @@ describe("importing a renamed Claude session", () => { expect(imported.config.title).toBe("Scrollback review"); }); + + test("imports a large record after the last rename without repeatedly copying it", async () => { + const sessionId = "large-record-after-rename"; + await writeSession({ + sessionId, + records: [ + { type: "custom-title", customTitle: "Large attachment review", sessionId }, + userRecord({ sessionId, content: "x".repeat(96 * 1024 * 1024) }), + ], + }); + + const imported = await importSession(sessionId); + + expect(imported.config.title).toBe("Large attachment review"); + }); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 64cbdb6777..ebb37e13f8 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -6878,39 +6878,6 @@ function readClaudeCustomTitleFromRecord(record: Buffer): string | null { ); } -/** - * Reads the records of one window back to front, reporting where the first of - * them starts: what precedes that is a record the window cut in half, which the - * read before it completes. - */ -function scanClaudeCustomTitleBackwards(window: Buffer): { - title: string | null; - recordStart: number; -} { - if (!window.includes(CLAUDE_CUSTOM_TITLE_NEEDLE)) { - // Nothing here to parse. A record the window cut in half is carried over - // whole rather than split, so a needle inside it is still seen once the read - // before it completes that record. - const firstNewline = window.indexOf(10); - return { title: null, recordStart: firstNewline === -1 ? window.length : firstNewline }; - } - // A newline never falls inside a UTF-8 sequence, so splitting the bytes on it - // leaves every complete record decodable on its own. - let recordEnd = window.length; - for ( - let at = window.lastIndexOf(10); - at >= 0; - at = at === 0 ? -1 : window.lastIndexOf(10, at - 1) - ) { - const title = readClaudeCustomTitleFromRecord(window.subarray(at + 1, recordEnd)); - if (title) { - return { title, recordStart: recordEnd }; - } - recordEnd = at; - } - return { title: null, recordStart: recordEnd }; -} - /** * The `custom-title` record is re-emitted every time the session is saved, so * the current name is the last usable one in the file. Reading backwards a chunk @@ -6931,23 +6898,31 @@ async function readLastClaudeCustomTitle(filePath: string): Promise 0) { const length = Math.min(CLAUDE_TITLE_SCAN_CHUNK_BYTES, end); const chunk = Buffer.alloc(length); await handle.read(chunk, 0, length, end - length); - const window = pending.length > 0 ? Buffer.concat([chunk, pending]) : chunk; - const scan = scanClaudeCustomTitleBackwards(window); - if (scan.title) { - return scan.title; + let recordEnd = chunk.length; + for ( + let at = chunk.lastIndexOf(10); + at >= 0; + at = at === 0 ? -1 : chunk.lastIndexOf(10, at - 1) + ) { + pieces.push(chunk.subarray(at + 1, recordEnd)); + const record = Buffer.concat(pieces.toReversed()); + const title = readClaudeCustomTitleFromRecord(record); + if (title) return title; + pieces = []; + recordEnd = at; } - pending = Buffer.from(window.subarray(0, scan.recordStart)); + if (recordEnd > 0) pieces.push(chunk.subarray(0, recordEnd)); end -= length; } // The first record in the file has no newline in front of it. - return readClaudeCustomTitleFromRecord(pending); + return readClaudeCustomTitleFromRecord(Buffer.concat(pieces.toReversed())); } finally { await handle.close(); }