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
This commit is contained in:
David Brailovsky 2026-09-25 10:53:32 +02:00
parent 5fd446ca18
commit f444ea5eaf
3 changed files with 34 additions and 3 deletions

View file

@ -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

View file

@ -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 ?? "");

View file

@ -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");