From c4e01860f7f79220e5f242077d68bf041fb0b2d7 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 6 Aug 2026 17:44:41 +0000 Subject: [PATCH] fix(server): preserve live instruction locks Co-Authored-By: Paperclip --- .../__tests__/agent-instructions-service.test.ts | 7 +++++-- server/src/services/agent-instructions.ts | 13 ++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/server/src/__tests__/agent-instructions-service.test.ts b/server/src/__tests__/agent-instructions-service.test.ts index beb7fafe75d..6483707b1a6 100644 --- a/server/src/__tests__/agent-instructions-service.test.ts +++ b/server/src/__tests__/agent-instructions-service.test.ts @@ -667,7 +667,7 @@ describe("agent instructions service", () => { }); it.skipIf(process.platform !== "linux")( - "does not evict a live lock when process identity falls back to another scheme", + "does not evict a live lock with a stale heartbeat when identity falls back to another scheme", async () => { const paperclipHome = await makeTempDir("paperclip-agent-instructions-marker-fallback-"); cleanupDirs.add(paperclipHome); @@ -698,7 +698,10 @@ describe("agent instructions service", () => { })}\n`, "utf8", ); - await fs.writeFile(path.join(lockPath, `heartbeat-${token}`), "", "utf8"); + const heartbeatPath = path.join(lockPath, `heartbeat-${token}`); + await fs.writeFile(heartbeatPath, "", "utf8"); + const staleHeartbeat = new Date(Date.now() - 60_000); + await fs.utimes(heartbeatPath, staleHeartbeat, staleHeartbeat); const materialization = svc.materializeManagedBundle(agent, { "AGENTS.md": "# Stock\n" }); const firstOutcome = await Promise.race([ diff --git a/server/src/services/agent-instructions.ts b/server/src/services/agent-instructions.ts index 4fded35503e..f7b165d806f 100644 --- a/server/src/services/agent-instructions.ts +++ b/server/src/services/agent-instructions.ts @@ -278,15 +278,11 @@ async function removeStaleManagedBundleLock(lockPath: string): Promise ownerSnapshot = await fs.readFile(path.join(lockPath, "owner.json"), "utf8"); const owner = JSON.parse(ownerSnapshot) as { pid?: unknown; - token?: unknown; processStartMarker?: unknown; }; const pid = typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 ? owner.pid : null; - const token = typeof owner.token === "string" && /^[0-9a-f-]{36}$/i.test(owner.token) - ? owner.token - : null; if (pid !== null && isProcessAlive(pid)) { const recordedStart = typeof owner.processStartMarker === "string" ? owner.processStartMarker @@ -298,12 +294,11 @@ async function removeStaleManagedBundleLock(lockPath: string): Promise && processStartMarkersComparable(recordedStart, currentStart) ) { shouldRemove = recordedStart !== currentStart; - } else if (token) { - shouldRemove = await managedBundleLockAgeMs( - path.join(lockPath, `heartbeat-${token}`), - ) > MANAGED_BUNDLE_LOCK_STALE_MS; } else { - shouldRemove = await managedBundleLockAgeMs(lockPath) > MANAGED_BUNDLE_LOCK_STALE_MS; + // A delayed heartbeat does not prove that a live process has stopped + // materializing. Only a comparable process-start marker can safely + // distinguish a reused PID from the original owner. + shouldRemove = false; } } else if (pid !== null) { shouldRemove = true;