diff --git a/server/src/__tests__/agent-instructions-service.test.ts b/server/src/__tests__/agent-instructions-service.test.ts index 89951a7442a..c71afe572a8 100644 --- a/server/src/__tests__/agent-instructions-service.test.ts +++ b/server/src/__tests__/agent-instructions-service.test.ts @@ -489,6 +489,44 @@ describe("agent instructions service", () => { await expect(fs.stat(lockPath)).rejects.toThrow(); }); + it("does not evict an old materialization lock owned by a live process", async () => { + const paperclipHome = await makeTempDir("paperclip-agent-instructions-live-lock-"); + cleanupDirs.add(paperclipHome); + process.env.PAPERCLIP_HOME = paperclipHome; + process.env.PAPERCLIP_INSTANCE_ID = "test-instance"; + const svc = agentInstructionsService(); + const agent = makeAgent({}); + const managedRoot = path.join( + paperclipHome, + "instances", + "test-instance", + "companies", + "company-1", + "agents", + "agent-1", + "instructions", + ); + const lockPath = `${managedRoot}.paperclip-materialize.lock`; + await fs.mkdir(lockPath, { recursive: true }); + await fs.writeFile( + path.join(lockPath, "owner.json"), + `${JSON.stringify({ pid: process.pid, token: "live", createdAt: "1970-01-01T00:00:00.000Z" })}\n`, + "utf8", + ); + + const materialization = svc.materializeManagedBundle(agent, { "AGENTS.md": "# Stock\n" }); + const firstOutcome = await Promise.race([ + materialization.then(() => "materialized"), + new Promise<"waiting">((resolve) => setTimeout(() => resolve("waiting"), 75)), + ]); + + expect(firstOutcome).toBe("waiting"); + await fs.rm(lockPath, { recursive: true, force: true }); + await expect(materialization).resolves.toMatchObject({ + materialization: { action: "added" }, + }); + }); + it("canonicalizes line endings and ignores transient files when classifying drift", async () => { const paperclipHome = await makeTempDir("paperclip-agent-instructions-canonical-"); cleanupDirs.add(paperclipHome); diff --git a/server/src/services/agent-instructions.ts b/server/src/services/agent-instructions.ts index b0cbaacd364..6e416a0eaa4 100644 --- a/server/src/services/agent-instructions.ts +++ b/server/src/services/agent-instructions.ts @@ -192,14 +192,16 @@ async function removeStaleManagedBundleLock(lockPath: string): Promise try { const owner = JSON.parse(await fs.readFile(path.join(lockPath, "owner.json"), "utf8")) as { pid?: unknown; - createdAt?: unknown; }; - const pid = typeof owner.pid === "number" ? owner.pid : 0; - const createdAt = typeof owner.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN; - const ageMs = Number.isFinite(createdAt) - ? Date.now() - createdAt - : MANAGED_BUNDLE_LOCK_STALE_MS + 1; - shouldRemove = !isProcessAlive(pid) || ageMs > MANAGED_BUNDLE_LOCK_STALE_MS; + const pid = typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 + ? owner.pid + : null; + if (pid === null) { + const stat = await statIfExists(lockPath); + shouldRemove = !stat || Date.now() - stat.mtimeMs > MANAGED_BUNDLE_LOCK_STALE_MS; + } else { + shouldRemove = !isProcessAlive(pid); + } } catch { const stat = await statIfExists(lockPath); shouldRemove = !stat || Date.now() - stat.mtimeMs > MANAGED_BUNDLE_LOCK_STALE_MS;