From 695cea68f546b3d7eac19caf80ec91faaff98efa Mon Sep 17 00:00:00 2001 From: Rohit <76606932+rohitjavvadi@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:51:15 +0530 Subject: [PATCH] Fix recent session resume with long headers (#94578) Merged via squash. Prepared head SHA: 810296118468ba0a14fba7ba7e9b9c81ac3328b0 Co-authored-by: rohitjavvadi <76606932+rohitjavvadi@users.noreply.github.com> Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> Reviewed-by: @vincentkoc --- src/agents/sessions/session-manager.test.ts | 67 +++++++++++++++++++++ src/agents/sessions/session-manager.ts | 36 +++++++++-- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/agents/sessions/session-manager.test.ts b/src/agents/sessions/session-manager.test.ts index 9e00ea7cf6ff..52d64fb1413c 100644 --- a/src/agents/sessions/session-manager.test.ts +++ b/src/agents/sessions/session-manager.test.ts @@ -11,6 +11,7 @@ import { prepareSessionManagerForRun } from "../embedded-agent-runner/session-ma import { repairSessionFileIfNeeded } from "../session-file-repair.js"; import { CURRENT_SESSION_VERSION, + findMostRecentSession, loadEntriesFromFile, SessionManager, type SessionEntry, @@ -122,6 +123,72 @@ describe("SessionManager.open", () => { expect(entries.filter((entry) => entry.type === "session")).toHaveLength(1); }); + it("continues a valid recent session when the header exceeds the first read chunk", async () => { + const dir = await makeTempDir(); + const sessionFile = path.join(dir, "long-header-session.jsonl"); + const longCwd = `/tmp/${"deep/".repeat(120)}`; + const header = { + type: "session", + version: CURRENT_SESSION_VERSION, + id: "long-header-session", + timestamp: "2026-06-18T00:00:00.000Z", + cwd: longCwd, + }; + const userEntry = { + type: "message", + id: "user-1", + parentId: null, + timestamp: "2026-06-18T00:00:01.000Z", + message: { role: "user", content: "resume me" }, + }; + await fs.writeFile( + sessionFile, + `${JSON.stringify(header)}\n${JSON.stringify(userEntry)}\n`, + "utf8", + ); + + expect(Buffer.byteLength(JSON.stringify(header), "utf8")).toBeGreaterThan(512); + expect(loadEntriesFromFile(sessionFile)).toHaveLength(2); + expect(findMostRecentSession(dir)).toBe(sessionFile); + expect(SessionManager.continueRecent(longCwd, dir).getSessionFile()).toBe(sessionFile); + }); + + it("skips oversized recent session headers instead of hiding valid sessions", async () => { + const dir = await makeTempDir(); + const validSessionFile = path.join(dir, "valid-session.jsonl"); + const oversizedSessionFile = path.join(dir, "oversized-header-session.jsonl"); + const validHeader = { + type: "session", + version: CURRENT_SESSION_VERSION, + id: "valid-session", + timestamp: "2026-06-18T00:00:00.000Z", + cwd: "/tmp/task-repo", + }; + const oversizedHeader = { + type: "session", + version: CURRENT_SESSION_VERSION, + id: "oversized-header-session", + timestamp: "2026-06-18T00:00:01.000Z", + cwd: `/tmp/${"deep/".repeat(14_000)}`, + }; + + await fs.writeFile(validSessionFile, `${JSON.stringify(validHeader)}\n`, "utf8"); + await fs.writeFile(oversizedSessionFile, `${JSON.stringify(oversizedHeader)}\n`, "utf8"); + await fs.utimes( + validSessionFile, + new Date("2026-06-18T00:00:00.000Z"), + new Date("2026-06-18T00:00:00.000Z"), + ); + await fs.utimes( + oversizedSessionFile, + new Date("2026-06-18T00:00:01.000Z"), + new Date("2026-06-18T00:00:01.000Z"), + ); + + expect(Buffer.byteLength(JSON.stringify(oversizedHeader), "utf8")).toBeGreaterThan(64 * 1024); + expect(findMostRecentSession(dir)).toBe(validSessionFile); + }); + it("still migrates old transcript versions while bypassing the warm cache", async () => { const dir = await makeTempDir(); const sessionFile = path.join(dir, "session.jsonl"); diff --git a/src/agents/sessions/session-manager.ts b/src/agents/sessions/session-manager.ts index e25ec6a41081..79333e0312f5 100644 --- a/src/agents/sessions/session-manager.ts +++ b/src/agents/sessions/session-manager.ts @@ -50,6 +50,9 @@ import type { BashExecutionMessage, CustomMessage } from "./messages.js"; export { CURRENT_SESSION_VERSION }; +const SESSION_HEADER_READ_CHUNK_BYTES = 4096; +const MAX_SESSION_HEADER_BYTES = 64 * 1024; + export interface SessionHeader { type: "session"; version?: number; // v1 sessions don't have this @@ -1141,13 +1144,36 @@ function recoverCorruptSessionEntries(filePath: string, cwd: string): FileEntry[ return [header, ...recoveredEntries]; } +function readFirstSessionFileLine(filePath: string): string | undefined { + const fd = openSync(filePath, "r"); + try { + const chunks: Buffer[] = []; + let totalBytes = 0; + while (totalBytes < MAX_SESSION_HEADER_BYTES) { + const buffer = Buffer.alloc( + Math.min(SESSION_HEADER_READ_CHUNK_BYTES, MAX_SESSION_HEADER_BYTES - totalBytes), + ); + const bytesRead = readSync(fd, buffer, 0, buffer.length, totalBytes); + if (bytesRead === 0) { + break; + } + const newlineIndex = buffer.indexOf(0x0a); + if (newlineIndex >= 0 && newlineIndex < bytesRead) { + chunks.push(buffer.subarray(0, newlineIndex)); + return Buffer.concat(chunks).toString("utf8"); + } + chunks.push(buffer.subarray(0, bytesRead)); + totalBytes += bytesRead; + } + return chunks.length > 0 ? Buffer.concat(chunks).toString("utf8") : undefined; + } finally { + closeSync(fd); + } +} + function isValidSessionFile(filePath: string): boolean { try { - const fd = openSync(filePath, "r"); - const buffer = Buffer.alloc(512); - const bytesRead = readSync(fd, buffer, 0, 512, 0); - closeSync(fd); - const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0]; + const firstLine = readFirstSessionFileLine(filePath); if (!firstLine) { return false; }