Skip to content

Commit 8102961

Browse files
rohitjavvadivincentkoc
authored andcommitted
Fix recent session resume with long headers
1 parent ca5905e commit 8102961

2 files changed

Lines changed: 98 additions & 5 deletions

File tree

src/agents/sessions/session-manager.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { prepareSessionManagerForRun } from "../embedded-agent-runner/session-ma
1111
import { repairSessionFileIfNeeded } from "../session-file-repair.js";
1212
import {
1313
CURRENT_SESSION_VERSION,
14+
findMostRecentSession,
1415
loadEntriesFromFile,
1516
SessionManager,
1617
type SessionEntry,
@@ -122,6 +123,72 @@ describe("SessionManager.open", () => {
122123
expect(entries.filter((entry) => entry.type === "session")).toHaveLength(1);
123124
});
124125

126+
it("continues a valid recent session when the header exceeds the first read chunk", async () => {
127+
const dir = await makeTempDir();
128+
const sessionFile = path.join(dir, "long-header-session.jsonl");
129+
const longCwd = `/tmp/${"deep/".repeat(120)}`;
130+
const header = {
131+
type: "session",
132+
version: CURRENT_SESSION_VERSION,
133+
id: "long-header-session",
134+
timestamp: "2026-06-18T00:00:00.000Z",
135+
cwd: longCwd,
136+
};
137+
const userEntry = {
138+
type: "message",
139+
id: "user-1",
140+
parentId: null,
141+
timestamp: "2026-06-18T00:00:01.000Z",
142+
message: { role: "user", content: "resume me" },
143+
};
144+
await fs.writeFile(
145+
sessionFile,
146+
`${JSON.stringify(header)}\n${JSON.stringify(userEntry)}\n`,
147+
"utf8",
148+
);
149+
150+
expect(Buffer.byteLength(JSON.stringify(header), "utf8")).toBeGreaterThan(512);
151+
expect(loadEntriesFromFile(sessionFile)).toHaveLength(2);
152+
expect(findMostRecentSession(dir)).toBe(sessionFile);
153+
expect(SessionManager.continueRecent(longCwd, dir).getSessionFile()).toBe(sessionFile);
154+
});
155+
156+
it("skips oversized recent session headers instead of hiding valid sessions", async () => {
157+
const dir = await makeTempDir();
158+
const validSessionFile = path.join(dir, "valid-session.jsonl");
159+
const oversizedSessionFile = path.join(dir, "oversized-header-session.jsonl");
160+
const validHeader = {
161+
type: "session",
162+
version: CURRENT_SESSION_VERSION,
163+
id: "valid-session",
164+
timestamp: "2026-06-18T00:00:00.000Z",
165+
cwd: "/tmp/task-repo",
166+
};
167+
const oversizedHeader = {
168+
type: "session",
169+
version: CURRENT_SESSION_VERSION,
170+
id: "oversized-header-session",
171+
timestamp: "2026-06-18T00:00:01.000Z",
172+
cwd: `/tmp/${"deep/".repeat(14_000)}`,
173+
};
174+
175+
await fs.writeFile(validSessionFile, `${JSON.stringify(validHeader)}\n`, "utf8");
176+
await fs.writeFile(oversizedSessionFile, `${JSON.stringify(oversizedHeader)}\n`, "utf8");
177+
await fs.utimes(
178+
validSessionFile,
179+
new Date("2026-06-18T00:00:00.000Z"),
180+
new Date("2026-06-18T00:00:00.000Z"),
181+
);
182+
await fs.utimes(
183+
oversizedSessionFile,
184+
new Date("2026-06-18T00:00:01.000Z"),
185+
new Date("2026-06-18T00:00:01.000Z"),
186+
);
187+
188+
expect(Buffer.byteLength(JSON.stringify(oversizedHeader), "utf8")).toBeGreaterThan(64 * 1024);
189+
expect(findMostRecentSession(dir)).toBe(validSessionFile);
190+
});
191+
125192
it("still migrates old transcript versions while bypassing the warm cache", async () => {
126193
const dir = await makeTempDir();
127194
const sessionFile = path.join(dir, "session.jsonl");

src/agents/sessions/session-manager.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ import type { BashExecutionMessage, CustomMessage } from "./messages.js";
5050

5151
export { CURRENT_SESSION_VERSION };
5252

53+
const SESSION_HEADER_READ_CHUNK_BYTES = 4096;
54+
const MAX_SESSION_HEADER_BYTES = 64 * 1024;
55+
5356
export interface SessionHeader {
5457
type: "session";
5558
version?: number; // v1 sessions don't have this
@@ -1141,13 +1144,36 @@ function recoverCorruptSessionEntries(filePath: string, cwd: string): FileEntry[
11411144
return [header, ...recoveredEntries];
11421145
}
11431146

1144-
function isValidSessionFile(filePath: string): boolean {
1147+
function readFirstSessionFileLine(filePath: string): string | undefined {
1148+
const fd = openSync(filePath, "r");
11451149
try {
1146-
const fd = openSync(filePath, "r");
1147-
const buffer = Buffer.alloc(512);
1148-
const bytesRead = readSync(fd, buffer, 0, 512, 0);
1150+
const chunks: Buffer[] = [];
1151+
let totalBytes = 0;
1152+
while (totalBytes < MAX_SESSION_HEADER_BYTES) {
1153+
const buffer = Buffer.alloc(
1154+
Math.min(SESSION_HEADER_READ_CHUNK_BYTES, MAX_SESSION_HEADER_BYTES - totalBytes),
1155+
);
1156+
const bytesRead = readSync(fd, buffer, 0, buffer.length, totalBytes);
1157+
if (bytesRead === 0) {
1158+
break;
1159+
}
1160+
const newlineIndex = buffer.indexOf(0x0a);
1161+
if (newlineIndex >= 0 && newlineIndex < bytesRead) {
1162+
chunks.push(buffer.subarray(0, newlineIndex));
1163+
return Buffer.concat(chunks).toString("utf8");
1164+
}
1165+
chunks.push(buffer.subarray(0, bytesRead));
1166+
totalBytes += bytesRead;
1167+
}
1168+
return chunks.length > 0 ? Buffer.concat(chunks).toString("utf8") : undefined;
1169+
} finally {
11491170
closeSync(fd);
1150-
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0];
1171+
}
1172+
}
1173+
1174+
function isValidSessionFile(filePath: string): boolean {
1175+
try {
1176+
const firstLine = readFirstSessionFileLine(filePath);
11511177
if (!firstLine) {
11521178
return false;
11531179
}

0 commit comments

Comments
 (0)