paperclipai/paperclip · error · Error
ACPX module changed during snapshot
Error message
ACPX module changed during snapshot
What it means
After reading a module's bytes, copy() re-stats both the open handle and the source path and compares with the original lstat. If either changed, the bytes read may not represent any coherent version of the file, so it throws 'ACPX module changed during snapshot'. Together with the before-open check, this brackets the read with integrity verification.
Source
Thrown at packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts:136
}
if (!before.isFile() || before.size > 16n * 1024n * 1024n)
throw new Error("ACPX module must be a bounded regular file");
bytesCopied += Number(before.size);
if (bytesCopied > MAX_PACKAGE_SNAPSHOT_BYTES)
throw new Error("ACPX package snapshot exceeds its byte bound");
const handle = await open(
source,
constants.O_RDONLY | constants.O_NOFOLLOW,
);
try {
if (!same(before, await handle.stat({ bigint: true })))
throw new Error("ACPX module changed before snapshot");
const bytes = await readSnapshotBytes(handle, Number(before.size));
if (
!same(before, await handle.stat({ bigint: true })) ||
!same(before, await lstat(source, { bigint: true }))
) {
throw new Error("ACPX module changed during snapshot");
}
await writeFile(target, bytes, { flag: "wx", mode: 0o400 });
digests[target] = digest(bytes);
} finally {
await handle.close();
}
};
try {
for (let index = 0; index < sourceRoots.length; index++) {
await copy(sourceRoots[index]!, roots[index]!, sourceRoots[index]!);
if (
!same(
sourceIdentities[index]!,
await lstat(sourceRoots[index]!, { bigint: true }),
)
) {
throw new Error("ACPX package root changed during snapshot");
}View on GitHub (pinned to 01ad858492)
Solutions
- Stop all writers to the package tree before snapshotting and retry
- Move runtime-writable locations (logs, caches) out of the admitted package roots
- Retry the snapshot once the directory is quiet; treat repeated occurrences as a misconfiguration of which roots are admitted
- Use immutable inputs (clean checkout or container image) as snapshot sources
Example fix
// before await Promise.all([snapshot(roots), runTests(roots)]); // after await runTests(roots); await snapshot(roots);
Defensive patterns
Strategy: retry
Validate before calling
const st1 = await fs.stat(file);
await new Promise(r => setTimeout(r, 200));
const st2 = await fs.stat(file);
if (st1.mtimeMs !== st2.mtimeMs) throw new Error('writers still active on package tree'); Try / catch
try {
await createAcpxPrivateSnapshot({ roots });
} catch (e) {
if (e.message.includes('changed during snapshot')) {
await quiesceWriters(roots);
await createAcpxPrivateSnapshot({ roots });
} else throw e;
} Prevention
- Keep logs, caches, and test outputs out of package roots
- Disable format-on-save/watchers for admitted trees during runs
- Snapshot once the workspace is idle; treat repeated races as root-selection mistakes
When it happens
Trigger: The file is modified (content, size, mtime, inode swap) by another process between the successful readSnapshotBytes and the post-read double stat inside createAcpxPrivateSnapshot.
Common situations: A watcher/linter/formatter saving files while sessions start; test runs writing logs into the package; log rotation or cache eviction touching files under the package root.
Related errors
- ACPX file ended during snapshot
- ACPX package directory changed during snapshot
- ACPX module changed before snapshot
- ACPX package root changed during snapshot
- ACPX ${agent} runtime executable changed while it was verifi
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/f0144dc38bcc9e91.
Report an issue: GitHub.