mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 13:24:55 +01:00
fix(server): recognize completed no-save directory cleanup
Preserve unresolved local copies as enrollment blockers while accepting the owner receipt for a stopped, fully cleaned-up rejected save. Verify both incomplete states and successful enrollment with real PostgreSQL/S3. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
6d31d1265d
commit
b32e90908f
2 files changed
+22
-2
No files matched your search
@@ -175,4 +175,20 @@ describe.skipIf(!process.env.PAPERCLIP_TEST_S3_ENDPOINT)("existing-volume enroll
|
||||
expect((await db.select().from(providerTraceRecords).where(eq(providerTraceRecords.companyId, companyId)))
|
||||
.every(trace => trace.status === "expired" && trace.deletedAt !== null)).toBe(true);
|
||||
}, 120_000);
|
||||
|
||||
it("accepts an unavailable directory only after its owner confirms stop and no-save cleanup", async () => {
|
||||
const row = await agent();
|
||||
const [run] = await db.insert(heartbeatRuns).values({ companyId, agentId: row.id, status: "succeeded" }).returning();
|
||||
await db.insert(agentInstructionWorkingCopies).values({ runId: run!.id, companyId, agentId: row.id,
|
||||
responsibleUserId: "fixture", entryFile: "AGENTS.md", baseHash: "fixture", localRoot: path.join(home, "rejected-copy"),
|
||||
executionRoot: path.join(home, "rejected-copy"), location: "local", state: "unavailable",
|
||||
errorCode: "AGENT_FILES_LIMIT_EXCEEDED", receipt: { schema: "paperclip.agent-files.v1", cleanupPending: true } });
|
||||
await expect(migrate()).rejects.toThrow("local_instruction_copies_require_settlement");
|
||||
await db.update(agentInstructionWorkingCopies).set({ receipt: { schema: "paperclip.agent-files.v1", cleanupPending: false } })
|
||||
.where(eq(agentInstructionWorkingCopies.runId, run!.id));
|
||||
await expect(migrate()).rejects.toThrow("local_instruction_copies_require_settlement");
|
||||
await db.update(agentInstructionWorkingCopies).set({ processStoppedAt: new Date() })
|
||||
.where(eq(agentInstructionWorkingCopies.runId, run!.id));
|
||||
expect((await migrate()).eligibility.eligible).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { execFile as execFileCallback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { and, asc, desc, eq, gt, inArray, notInArray, lte, or, isNull, sql, type SQL } from "drizzle-orm";
|
||||
import { and, asc, desc, eq, gt, inArray, not, notInArray, lte, or, isNull, isNotNull, sql, type SQL } from "drizzle-orm";
|
||||
import { agents, heartbeatRuns, agentInstructionWorkingCopies as copies, databaseWorkScopeKey, type Db } from "@paperclipai/db";
|
||||
import {
|
||||
prepareAdapterExecutionTargetRuntime,
|
||||
@@ -43,10 +43,14 @@ const copySaves = new WeakMap<object, Map<string, Promise<void>>>();
|
||||
|
||||
/** A terminal run does not establish that its private local edits were saved.
|
||||
* Enrollment may discard these paths only after this owner's save/resolution
|
||||
* or explicit ownership transfer. Unavailable/conflicted copies fail closed. */
|
||||
* or explicit ownership transfer. A rejected directory save is settled only
|
||||
* after this owner confirms both process stop and completed no-save cleanup. */
|
||||
export async function hasUnsettledLocalInstructionCopies(db: Db): Promise<boolean> {
|
||||
return (await db.select({ runId: copies.runId }).from(copies).where(and(
|
||||
eq(copies.location, "local"), notInArray(copies.state, ["saved", "unchanged", "resolved", "superseded"]),
|
||||
not(and(eq(copies.state, "unavailable"), isNotNull(copies.processStoppedAt),
|
||||
sql`coalesce(${copies.receipt}->>'schema', '') = 'paperclip.agent-files.v1'`,
|
||||
sql`coalesce(${copies.receipt}->>'cleanupPending', '') = 'false'`)),
|
||||
)).limit(1)).length > 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user