From f444ea5eafeb11dd02ea487f8916ba3a89540fb3 Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Fri, 25 Sep 2026 10:53:32 +0200 Subject: [PATCH] fix(coding-agent): use per-ref cache folders for pinned temporary git extensions Temporary git extensions (-e git:...@ref) were cached in a folder keyed only by host and repo path. After changing the pinned ref, pi reused the old checkout without refreshing it and kept loading the first downloaded commit. Include the ref in the temporary folder hash so each ref gets its own checkout. Unpinned sources keep their existing folder. closes #9982 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/package-manager.ts | 7 +++-- .../coding-agent/test/package-manager.test.ts | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e4d966d52..fb3c3ad48 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ - Fixed X11 clipboard text being misidentified as an image when the clipboard owner accepts unadvertised image targets ([#9786](https://github.com/earendil-works/pi/issues/9786)). - Prevented managed git packages from automatically installing Pi peer dependencies and added warnings for extension packages that list host-provided modules in `dependencies` ([#9863](https://github.com/earendil-works/pi/issues/9863)). +- Fixed pinned git extensions loaded with `-e` continuing to use the first downloaded commit after the ref changes ([#9982](https://github.com/earendil-works/pi/issues/9982)). ## [0.87.1] - 2026-09-22 diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index eb6f47825..be42365a9 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -2123,7 +2123,8 @@ export class DefaultPackageManager implements PackageManager { private getGitInstallPath(source: GitSource, scope: SourceScope): string { if (scope === "temporary") { - return this.getTemporaryDir(`git-${source.host}`, source.path); + // Include the ref in the hash so each pinned ref gets its own checkout. + return this.getTemporaryDir(`git-${source.host}`, source.path, source.ref); } const installRoot = this.getGitInstallRoot(scope); if (!installRoot) { @@ -2143,10 +2144,10 @@ export class DefaultPackageManager implements PackageManager { return join(this.agentDir, "git"); } - private getTemporaryDir(prefix: string, suffix?: string): string { + private getTemporaryDir(prefix: string, suffix?: string, ref?: string): string { const root = this.resolveManagedPath(getExtensionTempFolder(this.agentDir), prefix); const hash = createHash("sha256") - .update(`${prefix}-${suffix ?? ""}`) + .update(`${prefix}-${suffix ?? ""}${ref ? `@${ref}` : ""}`) .digest("hex") .slice(0, 8); return this.resolveManagedPath(root, hash, suffix ?? ""); diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index c93099b81..07cdc9d60 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -2615,6 +2615,35 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(refreshTemporaryGitSourceSpy).not.toHaveBeenCalled(); }); + // https://github.com/earendil-works/pi/issues/9982 + it("should load a new checkout when a pinned temporary git source changes ref", async () => { + const managerWithInternals = packageManager as unknown as PackageManagerInternals; + const oldSource = "git:github.com/example/repo@aaaaaaa"; + const newSource = "git:github.com/example/repo@bbbbbbb"; + const oldParsed = managerWithInternals.parseSource(oldSource); + const newParsed = managerWithInternals.parseSource(newSource); + if (oldParsed.type !== "git" || newParsed.type !== "git") { + throw new Error("Expected git sources"); + } + + const oldPath = managerWithInternals.getGitInstallPath(oldParsed, "temporary"); + mkdirSync(join(oldPath, "extensions"), { recursive: true }); + writeFileSync(join(oldPath, "extensions", "old.ts"), "export default function() {};"); + + const installParsedSourceSpy = vi + .spyOn(packageManager as any, "installParsedSource") + .mockImplementation(async () => { + const newPath = managerWithInternals.getGitInstallPath(newParsed, "temporary"); + mkdirSync(join(newPath, "extensions"), { recursive: true }); + writeFileSync(join(newPath, "extensions", "new.ts"), "export default function() {};"); + }); + + const result = await packageManager.resolveExtensionSources([newSource], { temporary: true }); + expect(installParsedSourceSpy).toHaveBeenCalledTimes(1); + expect(result.extensions.some((r) => pathEndsWith(r.path, "extensions/new.ts") && r.enabled)).toBe(true); + expect(result.extensions.some((r) => pathEndsWith(r.path, "extensions/old.ts"))).toBe(false); + }); + it("should not run npm view during resolve for installed unpinned packages", async () => { process.env.PI_OFFLINE = "1"; const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example");