{"record":{"id":"f0144dc38bcc9e91","repo":"paperclipai/paperclip","slug":"acpx-module-changed-during-snapshot","errorCode":null,"errorMessage":"ACPX module changed during snapshot","messagePattern":"ACPX module changed during snapshot","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts","lineNumber":136,"sourceCode":"    }\n    if (!before.isFile() || before.size > 16n * 1024n * 1024n)\n      throw new Error(\"ACPX module must be a bounded regular file\");\n    bytesCopied += Number(before.size);\n    if (bytesCopied > MAX_PACKAGE_SNAPSHOT_BYTES)\n      throw new Error(\"ACPX package snapshot exceeds its byte bound\");\n    const handle = await open(\n      source,\n      constants.O_RDONLY | constants.O_NOFOLLOW,\n    );\n    try {\n      if (!same(before, await handle.stat({ bigint: true })))\n        throw new Error(\"ACPX module changed before snapshot\");\n      const bytes = await readSnapshotBytes(handle, Number(before.size));\n      if (\n        !same(before, await handle.stat({ bigint: true })) ||\n        !same(before, await lstat(source, { bigint: true }))\n      ) {\n        throw new Error(\"ACPX module changed during snapshot\");\n      }\n      await writeFile(target, bytes, { flag: \"wx\", mode: 0o400 });\n      digests[target] = digest(bytes);\n    } finally {\n      await handle.close();\n    }\n  };\n  try {\n    for (let index = 0; index < sourceRoots.length; index++) {\n      await copy(sourceRoots[index]!, roots[index]!, sourceRoots[index]!);\n      if (\n        !same(\n          sourceIdentities[index]!,\n          await lstat(sourceRoots[index]!, { bigint: true }),\n        )\n      ) {\n        throw new Error(\"ACPX package root changed during snapshot\");\n      }","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts#L118-L154","documentation":"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.","triggerScenarios":"The file is modified (content, size, mtime, inode swap) by another process between the successful readSnapshotBytes and the post-read double stat inside createAcpxPrivateSnapshot.","commonSituations":"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.","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"],"exampleFix":"// before\nawait Promise.all([snapshot(roots), runTests(roots)]);\n// after\nawait runTests(roots);\nawait snapshot(roots);","handlingStrategy":"retry","validationCode":"const st1 = await fs.stat(file);\nawait new Promise(r => setTimeout(r, 200));\nconst st2 = await fs.stat(file);\nif (st1.mtimeMs !== st2.mtimeMs) throw new Error('writers still active on package tree');","typeGuard":null,"tryCatchPattern":"try {\n  await createAcpxPrivateSnapshot({ roots });\n} catch (e) {\n  if (e.message.includes('changed during snapshot')) {\n    await quiesceWriters(roots);\n    await createAcpxPrivateSnapshot({ roots });\n  } else throw e;\n}","preventionTips":["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"],"tags":["filesystem","race-condition","snapshot"],"backgroundTag":"file-changed-during-read","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}