diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index e8f524201d7..0523c4d09eb 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -345,6 +345,37 @@ describe("materializePaperclipSkillCopy", () => { } }); + it("does not lock an already-current materialized skill", async () => { + const root = await fs.mkdtemp( + path.join(os.tmpdir(), "paperclip-skill-copy-"), + ); + try { + const source = path.join(root, "source"); + const skillsHome = path.join(root, "skills"); + const target = path.join(skillsHome, "skill"); + const lock = `${target}.lock`; + await fs.mkdir(source, { recursive: true }); + await fs.writeFile(path.join(source, "SKILL.md"), "# skill\n", "utf8"); + await materializePaperclipSkillCopy(source, target); + + await fs.mkdir(lock); + const owner = path.join(lock, "owner.json"); + await fs.writeFile( + owner, + JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), + "utf8", + ); + + await expect( + materializePaperclipSkillCopy(source, target), + ).resolves.toMatchObject({ copiedFiles: 0 }); + expect((await fs.readdir(skillsHome)).sort()).toEqual(["skill", "skill.lock"]); + await expect(fs.readFile(owner, "utf8")).resolves.toContain(`"pid":${process.pid}`); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + it("breaks stale materialization locks left by dead processes", async () => { const root = await fs.mkdtemp( path.join(os.tmpdir(), "paperclip-skill-copy-"), diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 05a670d2724..26dac29cd9e 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -4563,6 +4563,10 @@ export async function materializePaperclipSkillCopy( skippedSymlinks: [], }; + const sourceFingerprint = await hashSkillDirectory(sourceRoot); + if (await materializedSkillFingerprintMatches(targetRoot, sourceFingerprint)) + return result; + const lockDir = `${targetRoot}.lock`; const releaseLock = await acquireMaterializeLock(lockDir); const tempRoot = `${targetRoot}.tmp-${process.pid}-${randomUUID()}`; @@ -4608,7 +4612,6 @@ export async function materializePaperclipSkillCopy( } try { - const sourceFingerprint = await hashSkillDirectory(sourceRoot); if ( await materializedSkillFingerprintMatches(targetRoot, sourceFingerprint) )