fix(server): serialize managed instruction swaps

Prevent same-agent materialization races and allocate force-reset backups under the same per-root queue.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-07 08:18:32 +00:00
1 parent af5000fce4
commit 128cadbbe8
2 files changed
+89 -2

No files matched your search

@@ -431,6 +431,33 @@ describe("agent instructions service", () => {
await expect(fs.stat(path.join(updated.bundle.managedRootPath, "docs", "OLD.md"))).rejects.toThrow();
});
it("serializes concurrent routine materializations for the same agent", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-concurrent-");
cleanupDirs.add(paperclipHome);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
const svc = agentInstructionsService();
const agent = makeAgent({});
const initial = await svc.materializeManagedBundle(agent, { "AGENTS.md": "# Stock v1\n" });
const configuredAgent = { ...agent, adapterConfig: initial.adapterConfig };
const [first, second] = await Promise.all([
svc.materializeManagedBundle(configuredAgent, { "AGENTS.md": "# Stock v2\n" }),
agentInstructionsService().materializeManagedBundle(configuredAgent, {
"AGENTS.md": "# Stock v3\n",
}),
]);
expect(first.materialization.action).toBe("updated");
expect(second.materialization).toMatchObject({
stockStatus: "operator_modified",
action: "skipped",
});
await expect(fs.readFile(path.join(initial.bundle.managedRootPath, "AGENTS.md"), "utf8")).resolves.toBe(
"# Stock v2\n",
);
});
it("canonicalizes line endings and ignores transient files when classifying drift", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-canonical-");
cleanupDirs.add(paperclipHome);
@@ -518,4 +545,34 @@ describe("agent instructions service", () => {
"# Second operator edit\n",
);
});
it("allocates distinct backups for concurrent force resets", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-concurrent-reset-");
cleanupDirs.add(paperclipHome);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
const svc = agentInstructionsService();
const agent = makeAgent({});
const initial = await svc.materializeManagedBundle(agent, { "AGENTS.md": "# Stock v1\n" });
const configuredAgent = { ...agent, adapterConfig: initial.adapterConfig };
const [first, second] = await Promise.all([
svc.forceResetManagedBundle(configuredAgent, { "AGENTS.md": "# Stock v2\n" }),
agentInstructionsService().forceResetManagedBundle(configuredAgent, {
"AGENTS.md": "# Stock v3\n",
}),
]);
expect(first.materialization.backupPath).toBe(`${initial.bundle.managedRootPath}.backup-1`);
expect(second.materialization.backupPath).toBe(`${initial.bundle.managedRootPath}.backup-2`);
await expect(
fs.readFile(path.join(`${initial.bundle.managedRootPath}.backup-1`, "AGENTS.md"), "utf8"),
).resolves.toBe("# Stock v1\n");
await expect(
fs.readFile(path.join(`${initial.bundle.managedRootPath}.backup-2`, "AGENTS.md"), "utf8"),
).resolves.toBe("# Stock v2\n");
await expect(fs.readFile(path.join(initial.bundle.managedRootPath, "AGENTS.md"), "utf8")).resolves.toBe(
"# Stock v3\n",
);
});
});