diff --git a/packages/coding-agent/src/internal-urls/local-protocol.ts b/packages/coding-agent/src/internal-urls/local-protocol.ts index a8f1fe1d0..75eadd6d9 100644 --- a/packages/coding-agent/src/internal-urls/local-protocol.ts +++ b/packages/coding-agent/src/internal-urls/local-protocol.ts @@ -34,9 +34,11 @@ function safeSessionId(options: LocalProtocolOptions): string { return safe.length > 0 ? safe : "session"; } -function shortLocalRoot(candidate: string, options: LocalProtocolOptions): string { - const hash = Bun.hash(candidate).toString(36); - return path.join(os.tmpdir(), "omp-local", `${safeSessionId(options)}-${hash}`); +function shortLocalRoot(options: LocalProtocolOptions): string { + // Derive the short root from the stable session id, never the artifact path, + // so `SessionManager.moveTo()` and the resume-after-move flow keep finding + // the same `local://` directory the session wrote pre-move. + return path.join(os.tmpdir(), "omp-local", safeSessionId(options)); } function getContentType(filePath: string): InternalResource["contentType"] { @@ -126,7 +128,7 @@ export function resolveLocalRoot(options: LocalProtocolOptions, platform: NodeJS if (artifactsDir) { const candidate = path.resolve(artifactsDir, "local"); if (platform === "win32" && candidate.length >= WINDOWS_LOCAL_ROOT_MAX_CHARS) { - return shortLocalRoot(candidate, options); + return shortLocalRoot(options); } return candidate; } diff --git a/packages/coding-agent/test/internal-urls/local-protocol.test.ts b/packages/coding-agent/test/internal-urls/local-protocol.test.ts index 6876a2ecc..7b8891686 100644 --- a/packages/coding-agent/test/internal-urls/local-protocol.test.ts +++ b/packages/coding-agent/test/internal-urls/local-protocol.test.ts @@ -90,8 +90,9 @@ describe("LocalProtocolHandler", () => { ); }); - it("uses a short temp root for long Windows artifact paths", async () => { + it("uses a stable short temp root for long Windows artifact paths", async () => { const longArtifactsDir = path.join(os.tmpdir(), "a".repeat(220), "artifacts"); + const expectedRoot = path.join(os.tmpdir(), "omp-local", "session_long"); const options = { getArtifactsDir: () => longArtifactsDir, getSessionId: () => "session:long", @@ -99,10 +100,17 @@ describe("LocalProtocolHandler", () => { const root = resolveLocalRoot(options, "win32"); const resolved = resolveLocalUrlToPath("local://memo.txt", options, "win32"); - expect(root).toContain(path.join("omp-local", "session_long-")); - expect(root).not.toContain(longArtifactsDir); - expect(root.length).toBeLessThan(path.join(longArtifactsDir, "local").length); - expect(resolved).toBe(path.join(root, "memo.txt")); + expect(root).toBe(expectedRoot); + expect(resolved).toBe(path.join(expectedRoot, "memo.txt")); + + // The short root must survive moves of the artifact directory so + // `local://PLAN.md` and handoff files written pre-move stay reachable + // after `SessionManager.moveTo()` updates `getArtifactsDir()`. + const movedOptions = { + getArtifactsDir: () => path.join(os.tmpdir(), "b".repeat(220), "artifacts"), + getSessionId: () => "session:long", + }; + expect(resolveLocalRoot(movedOptions, "win32")).toBe(expectedRoot); }); it("blocks symlink escapes outside local root", async () => {