mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
Fix recent session resume with long headers (#94578)
Merged via squash.
Prepared head SHA: 8102961184
Co-authored-by: rohitjavvadi <76606932+rohitjavvadi@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user