mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 19:26:12 +00:00
fix: fail closed on capped transcript header scan
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user