From 3be2964fc7dbd2a348f69cce6d950502ea7a62d4 Mon Sep 17 00:00:00 2001 From: Wynne668 Date: Sat, 1 Aug 2026 12:04:51 +0800 Subject: [PATCH] fix(sessions): repair migrated paths across short reads (#109400) --- src/commands/doctor-state-migrations.test.ts | 46 ++++++++++++++------ src/infra/state-migrations.session-store.ts | 3 +- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/commands/doctor-state-migrations.test.ts b/src/commands/doctor-state-migrations.test.ts index 157259782110..467e68d9e0dd 100644 --- a/src/commands/doctor-state-migrations.test.ts +++ b/src/commands/doctor-state-migrations.test.ts @@ -847,21 +847,39 @@ describe("doctor legacy state migrations", () => { }, }); - const detected = await detectLegacyStateMigrations({ - cfg: {}, - env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv, - }); - expect(detected.preview).toContain( - `- Sessions: repair migrated transcript paths in ${path.join(targetDir, "sessions.json")}`, - ); + const realReadSync = fs.readSync.bind(fs); + let shortReadCalls = 0; + const readSpy = vi.spyOn(fs, "readSync").mockImplementation((( + fd: number, + buffer: NodeJS.ArrayBufferView, + offset: number, + length: number, + position: fs.ReadPosition | null, + ) => { + shortReadCalls += 1; + return realReadSync(fd, buffer, offset, Math.min(length, 16), position); + }) as typeof fs.readSync); - const result = await runLegacyStateMigrations({ detected }); - expect(result.warnings).toStrictEqual([]); - expect(result.changes).toContain("Repaired migrated session transcript paths"); - const store = JSON.parse( - fs.readFileSync(path.join(targetDir, "sessions.json"), "utf8"), - ) as Record; - expect(store["agent:main:main"]).not.toHaveProperty("sessionFile"); + try { + const detected = await detectLegacyStateMigrations({ + cfg: {}, + env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv, + }); + expect(detected.preview).toContain( + `- Sessions: repair migrated transcript paths in ${path.join(targetDir, "sessions.json")}`, + ); + + const result = await runLegacyStateMigrations({ detected }); + expect(result.warnings).toStrictEqual([]); + expect(result.changes).toContain("Repaired migrated session transcript paths"); + const store = JSON.parse( + fs.readFileSync(path.join(targetDir, "sessions.json"), "utf8"), + ) as Record; + expect(store["agent:main:main"]).not.toHaveProperty("sessionFile"); + expect(shortReadCalls).toBeGreaterThan(1); + } finally { + readSpy.mockRestore(); + } }); it("does not bind stale session metadata to a colliding target transcript", async () => { diff --git a/src/infra/state-migrations.session-store.ts b/src/infra/state-migrations.session-store.ts index 2946912945ff..a83efbaad8f1 100644 --- a/src/infra/state-migrations.session-store.ts +++ b/src/infra/state-migrations.session-store.ts @@ -29,6 +29,7 @@ import { parseAgentSessionKey, } from "../routing/session-key.js"; import { normalizeSessionKeyPreservingOpaquePeerIds } from "../sessions/session-key-utils.js"; +import { readFileWindowFullySync } from "./file-read.js"; import { sameFileIdentity } from "./fs-safe-advanced.js"; import { expandHomePrefix } from "./home-dir.js"; import { isWithinDir } from "./path-safety.js"; @@ -434,7 +435,7 @@ export function resolveStaleLegacySessionFile(params: { const fd = fs.openSync(targetSessionFile, "r"); try { const buffer = Buffer.alloc(8192); - const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0); + const bytesRead = readFileWindowFullySync(fd, buffer, 0); if (bytesRead <= 0) { return undefined; }