fix(server): compare instruction lock identity schemes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-07 08:18:32 +00:00
1 parent c4e01860f7
commit 85c031ffa4
2 files changed
+67 -7

No files matched your search

@@ -620,7 +620,9 @@ describe("agent instructions service", () => {
]);
expect(firstOutcome).toBe("waiting");
await fs.rm(lockPath, { recursive: true, force: true });
const releasedLockPath = `${lockPath}.released`;
await fs.rename(lockPath, releasedLockPath);
await fs.rm(releasedLockPath, { recursive: true, force: true });
await expect(materialization).resolves.toMatchObject({
materialization: { action: "added" },
});
@@ -667,7 +669,7 @@ describe("agent instructions service", () => {
});
it.skipIf(process.platform !== "linux")(
"does not evict a live lock with a stale heartbeat when identity falls back to another scheme",
"does not evict a live lock with a stale heartbeat when its identity scheme is unavailable",
async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-marker-fallback-");
cleanupDirs.add(paperclipHome);
@@ -693,7 +695,7 @@ describe("agent instructions service", () => {
`${JSON.stringify({
pid: process.pid,
token,
processStartMarker: "ps:same-live-process-in-another-format",
processStartMarker: "unavailable:same-live-process-in-another-format",
createdAt: new Date().toISOString(),
})}\n`,
"utf8",
@@ -710,13 +712,56 @@ describe("agent instructions service", () => {
]);
expect(firstOutcome).toBe("waiting");
await fs.rm(lockPath, { recursive: true, force: true });
const releasedLockPath = `${lockPath}.released`;
await fs.rename(lockPath, releasedLockPath);
await fs.rm(releasedLockPath, { recursive: true, force: true });
await expect(materialization).resolves.toMatchObject({
materialization: { action: "added" },
});
},
);
it.skipIf(process.platform !== "linux")(
"recovers a reused PID when the owner recorded the ps fallback scheme",
async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-ps-reused-pid-");
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`;
const token = "00000000-0000-4000-8000-000000000000";
await fs.mkdir(lockPath, { recursive: true });
await fs.writeFile(
path.join(lockPath, "owner.json"),
`${JSON.stringify({
pid: process.pid,
token,
processStartMarker: "ps:not-the-current-process-start",
createdAt: new Date().toISOString(),
})}\n`,
"utf8",
);
await fs.writeFile(path.join(lockPath, `heartbeat-${token}`), "", "utf8");
const result = await svc.materializeManagedBundle(agent, { "AGENTS.md": "# Stock\n" });
expect(result.materialization.action).toBe("added");
await expect(fs.stat(lockPath)).rejects.toThrow();
},
);
it("canonicalizes line endings and ignores transient files when classifying drift", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-canonical-");
cleanupDirs.add(paperclipHome);
+18 -3
View File
@@ -189,9 +189,20 @@ function isProcessAlive(pid: number): boolean {
}
}
async function processStartMarker(pid: number): Promise<string | null> {
type ProcessStartMarkerScheme = "linux" | "ps";
function processStartMarkerScheme(marker: string): ProcessStartMarkerScheme | null {
if (marker.startsWith("linux:")) return "linux";
if (marker.startsWith("ps:")) return "ps";
return null;
}
async function processStartMarker(
pid: number,
preferredScheme?: ProcessStartMarkerScheme,
): Promise<string | null> {
if (!isProcessAlive(pid)) return null;
if (process.platform === "linux") {
if (process.platform === "linux" && preferredScheme !== "ps") {
try {
const stat = await fs.readFile(`/proc/${pid}/stat`, "utf8");
const commandEnd = stat.lastIndexOf(")");
@@ -203,8 +214,11 @@ async function processStartMarker(pid: number): Promise<string | null> {
} catch {
// Fall through to ps for non-standard procfs environments.
}
if (preferredScheme === "linux") return null;
}
if (preferredScheme === "linux") return null;
return new Promise((resolve) => {
execFile(
"ps",
@@ -287,7 +301,8 @@ async function removeStaleManagedBundleLock(lockPath: string): Promise<boolean>
const recordedStart = typeof owner.processStartMarker === "string"
? owner.processStartMarker
: null;
const currentStart = await processStartMarker(pid);
const recordedScheme = recordedStart ? processStartMarkerScheme(recordedStart) : null;
const currentStart = await processStartMarker(pid, recordedScheme ?? undefined);
if (
recordedStart
&& currentStart