fix: fail closed on capped transcript header scan

This commit is contained in:
momothemage
2026-07-03 11:59:22 +08:00
parent c226afe34e
commit f2e5cd1898
2 changed files with 72 additions and 7 deletions

View File

@@ -404,6 +404,57 @@ describe("prepareSessionManagerForRun", () => {
expect(sessionManager.flushed).toBe(true);
});
it("does not truncate when the first non-empty header is beyond the scan cap", async () => {
const sessionFile = await makeTempFile();
const blankPrefix = " \n".repeat(33_000);
const originalTranscript =
blankPrefix +
[
'{"type":"session","id":"broken"',
JSON.stringify({
type: "message",
id: "user-1",
parentId: null,
timestamp: "2026-05-27T00:00:01.000Z",
message: { role: "user", content: "persisted prompt" },
}),
].join("\n") +
"\n";
await fs.writeFile(sessionFile, originalTranscript, "utf-8");
const sessionManager = {
sessionId: "fresh-session",
cwd: "/srv/openclaw/main",
flushed: true,
fileEntries: [
{
type: "session",
id: "fresh-session",
cwd: "/srv/openclaw/main",
},
{
type: "message",
message: { role: "user" },
},
],
byId: new Map([["user-1", {}]]),
labelsById: new Map(),
leafId: "user-1",
};
await expect(
prepareSessionManagerForRun({
sessionManager,
sessionFile,
hadSessionFile: true,
sessionId: "new-session",
cwd: "/tmp/task-repo",
}),
).rejects.toThrow("Refusing to reset session transcript before finding a readable header");
expect(await fs.readFile(sessionFile, "utf-8")).toBe(originalTranscript);
expect(sessionManager.flushed).toBe(true);
});
it("keeps recovered user-only transcripts through open and run preparation", async () => {
const sessionFile = await makeTempFile();
const userEntry = {

View File

@@ -17,7 +17,12 @@ type SessionMessageEntry = { type: "message"; message?: { role?: string } };
const SESSION_HEADER_READ_CHUNK_BYTES = 4096;
const MAX_SESSION_HEADER_BYTES = 64 * 1024;
async function readFirstSessionFileLine(sessionFile: string): Promise<string | undefined> {
type SessionFileHeaderRead = {
firstLine?: string;
cappedBeforeFirstLine: boolean;
};
async function readFirstSessionFileLine(sessionFile: string): Promise<SessionFileHeaderRead> {
const handle = await fs.open(sessionFile, "r");
try {
const chunks: string[] = [];
@@ -31,8 +36,8 @@ async function readFirstSessionFileLine(sessionFile: string): Promise<string | u
break;
}
chunks.push(buffer.toString("utf-8", 0, bytesRead));
const readEnd =
bytesRead < buffer.length || totalBytes + bytesRead >= MAX_SESSION_HEADER_BYTES;
const nextTotalBytes = totalBytes + bytesRead;
const readEnd = bytesRead < buffer.length || nextTotalBytes >= MAX_SESSION_HEADER_BYTES;
const scannedText = chunks.join("");
const lines = scannedText.split(/\r?\n/u);
const hasCompleteLine = lines.length > 1;
@@ -40,23 +45,32 @@ async function readFirstSessionFileLine(sessionFile: string): Promise<string | u
for (const line of linesToScan) {
const trimmed = line.trim();
if (trimmed) {
return trimmed;
return { firstLine: trimmed, cappedBeforeFirstLine: false };
}
}
totalBytes += bytesRead;
totalBytes = nextTotalBytes;
if (totalBytes >= MAX_SESSION_HEADER_BYTES) {
return { cappedBeforeFirstLine: true };
}
}
return chunks
const firstLine = chunks
.join("")
.split(/\r?\n/u)
.map((line) => line.trim())
.find((line) => line.length > 0);
return { firstLine, cappedBeforeFirstLine: false };
} finally {
await handle.close().catch(() => undefined);
}
}
async function assertExistingHeaderIsReadable(sessionFile: string): Promise<void> {
const firstLine = await readFirstSessionFileLine(sessionFile);
const { firstLine, cappedBeforeFirstLine } = await readFirstSessionFileLine(sessionFile);
if (cappedBeforeFirstLine) {
throw new Error(
`Refusing to reset session transcript before finding a readable header: ${sessionFile}`,
);
}
if (!firstLine) {
return;
}