mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 06:16:06 -10:00
fix: avoid synthetic dates on restored agent files
Co-Authored-By: Paperclip <[email protected]>
This commit is contained in:
1 parent
254e872fcf
commit
f31405a730
2 files changed
+32
-5
No files matched your search
@@ -111,6 +111,31 @@ describe("durable agent file authority", () => {
|
||||
} })).toEqual({ entryFile: "missing.md", contentHash: null });
|
||||
});
|
||||
|
||||
it("keeps recorded file dates through restore without treating projection times as edits", async () => {
|
||||
await materializeInstructionBytes(root, "AGENTS.md", Buffer.from("Historical instructions"));
|
||||
await db.update(agents).set({ adapterConfig: { instructionsBundleMode: "managed", instructionsRootPath: root,
|
||||
instructionsEntryFile: "AGENTS.md" } }).where(eq(agents.id, agentId));
|
||||
const [agent] = await db.select().from(agents).where(eq(agents.id, agentId));
|
||||
await store().read(companyId, agentId, "AGENTS.md", actor());
|
||||
await store().write({ companyId, agentId, path: "NOTES.md", bytes: Buffer.from("Recorded creation"), baseHash: null }, actor());
|
||||
const instructions = agentInstructionsService(db);
|
||||
const created = await instructions.readFile(agent, "NOTES.md");
|
||||
expect(Number.isFinite(Date.parse(created.createdAt!))).toBe(true);
|
||||
await replaceFilesystem();
|
||||
vi.stubEnv("PAPERCLIP_DURABLE_AGENT_FILES", "false");
|
||||
const restored = await instructions.readFile(agent, "AGENTS.md");
|
||||
expect(restored.content).toBe("Historical instructions");
|
||||
expect(restored.createdAt).toBeUndefined();
|
||||
expect(restored.updatedAt).toBeUndefined();
|
||||
const summary = (await instructions.getBundle(agent)).files.find(file => file.path === "AGENTS.md");
|
||||
expect(summary).toBeDefined();
|
||||
expect(summary?.createdAt).toBeUndefined();
|
||||
expect(summary?.updatedAt).toBeUndefined();
|
||||
const recorded = await instructions.readFile(agent, "NOTES.md");
|
||||
expect(recorded.createdAt).toBe(created.createdAt);
|
||||
expect(recorded.updatedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it("imports legacy bytes, modes and empty entries, then restores without any old filesystem", async () => {
|
||||
const binary = Buffer.alloc(2 * 1024 * 1024, 0x81);
|
||||
await materializeInstructionBytes(root, "AGENTS.md", Buffer.from("Original"));
|
||||
|
||||
@@ -221,12 +221,12 @@ async function listFilesRecursive(
|
||||
return output.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
async function readFileSummary(rootPath: string, relativePath: string, entryFile: string): Promise<AgentInstructionsFileSummary> {
|
||||
async function readFileSummary(rootPath: string, relativePath: string, entryFile: string, includeFilesystemDate: boolean): Promise<AgentInstructionsFileSummary> {
|
||||
const absolutePath = resolvePathWithinRoot(rootPath, relativePath);
|
||||
const stat = await fs.stat(absolutePath);
|
||||
// External bundles may contain large assets; listing must not read them.
|
||||
const file = stat.size > MAX_AGENT_FILE_BYTES ? null : await inspectAgentFile(rootPath, relativePath);
|
||||
return { ...summarizeFile(relativePath, entryFile, file?.size ?? stat.size, file?.bytes ?? null, file?.hash), updatedAt: stat.mtime.toISOString() };
|
||||
return { ...summarizeFile(relativePath, entryFile, file?.size ?? stat.size, file?.bytes ?? null, file?.hash), ...(includeFilesystemDate ? { updatedAt: stat.mtime.toISOString() } : {}) };
|
||||
}
|
||||
|
||||
function summarizeFile(relativePath: string, entryFile: string, size: number, bytes: Buffer | null, hash?: string): AgentInstructionsFileSummary {
|
||||
@@ -535,7 +535,7 @@ export function agentInstructionsService(db?: Db) {
|
||||
.orderBy(desc(activityLog.createdAt)).limit(1);
|
||||
if (saved) return saved.details?.created === true ? { createdAt: saved.date.toISOString() } : { updatedAt: saved.date.toISOString() };
|
||||
}
|
||||
// Filesystem birth times reset on atomic replacement/restoration. Only expose mtime.
|
||||
if (db && state.mode !== "external") return {};
|
||||
const stat = await statIfExists(filename);
|
||||
return stat?.isFile() ? { updatedAt: stat.mtime.toISOString() } : {};
|
||||
}
|
||||
@@ -577,7 +577,7 @@ export function agentInstructionsService(db?: Db) {
|
||||
const summaries: AgentInstructionsFileSummary[] = [];
|
||||
// Bound open descriptors and text buffers even for a large personal folder.
|
||||
for (let index = 0; index < files.length; index += 8) {
|
||||
summaries.push(...await Promise.all(files.slice(index, index + 8).map(relativePath => readFileSummary(state.rootPath!, relativePath, state.entryFile))));
|
||||
summaries.push(...await Promise.all(files.slice(index, index + 8).map(relativePath => readFileSummary(state.rootPath!, relativePath, state.entryFile, !db || state.mode === "external"))));
|
||||
}
|
||||
return toBundle(agent, state, summaries);
|
||||
}
|
||||
@@ -746,12 +746,14 @@ export function agentInstructionsService(db?: Db) {
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
const existed = Boolean(await statIfExists(absolutePath));
|
||||
await fs.writeFile(absolutePath, content, "utf8");
|
||||
const writtenAt = new Date().toISOString();
|
||||
const nextAgent = { ...agent, adapterConfig: prepared.adapterConfig };
|
||||
const [bundle, file] = await Promise.all([
|
||||
getBundle(nextAgent),
|
||||
readFile(nextAgent, relativePath),
|
||||
]);
|
||||
if (!existed && file.updatedAt) { file.createdAt = file.updatedAt; delete file.updatedAt; }
|
||||
if (existed) file.updatedAt = writtenAt;
|
||||
else { file.createdAt = writtenAt; delete file.updatedAt; }
|
||||
return { bundle, file, adapterConfig: prepared.adapterConfig };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user