{"record":{"id":"6726fda41caf7a54","repo":"paperclipai/paperclip","slug":"acpx-package-root-changed-during-snapshot","errorCode":null,"errorMessage":"ACPX package root changed during snapshot","messagePattern":"ACPX package root changed during snapshot","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts","lineNumber":153,"sourceCode":"      ) {\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      }\n    }\n    // Supply bare-package lookup links only for already admitted package roots.\n    const packages: Array<{ name: string; root: string }> = [];\n    for (const root of roots) {\n      const file = await open(join(root, \"package.json\")).catch(() => null);\n      if (!file) continue;\n      try {\n        const metadata = JSON.parse(await file.readFile(\"utf8\"));\n        if (\n          typeof metadata.name === \"string\" &&\n          /^(?:@[a-z0-9._-]+\\/)?[a-z0-9._-]+$/i.test(metadata.name)\n        ) {\n          packages.push({ name: metadata.name, root });\n        }\n      } finally {\n        await file.close();\n      }","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts#L135-L171","documentation":"After copying all roots, createAcpxPrivateSnapshot re-stats each admitted package root and compares it with the identity captured before the walk. If a root's identity changed during the snapshot — the package directory itself was replaced, renamed, or mutated — it throws. This is the top-level guard that the whole snapshot corresponds to one stable point in time.","triggerScenarios":"createAcpxPrivateSnapshot (called by privateSnapshot/snapshot) finishes copying roots, and the final lstat of any sourceRoots[i] no longer matches the sourceIdentities[i] captured at the start — e.g. the package directory was deleted, re-created by an install, or had entries added/removed during the walk.","commonSituations":"`npm install`/`pnpm install` rebuilding node_modules while a session starts; a sync tool (Dropbox, rsync job) mutating the workspace; a CI cache restore finishing mid-snapshot.","solutions":["Run the snapshot only after package installation has fully completed and the tree is idle","Retry the snapshot — the identity check is designed so a quiet re-run succeeds","Exclude the package root from background sync/scan tools during runs","Lock dependency installs so no concurrent process replaces package directories"],"exampleFix":"// before\nspawn('npm', ['install']);\nawait snapshot(roots);\n// after\nexecSync('npm install');\nawait snapshot(roots);","handlingStrategy":"retry","validationCode":"const ids = [];\nfor (const root of roots) ids.push(await fs.stat(root));\nawait new Promise(r => setTimeout(r, 300));\nfor (let i = 0; i < roots.length; i++) {\n  const now = await fs.stat(roots[i]);\n  if (now.ino !== ids[i].ino || now.mtimeMs !== ids[i].mtimeMs) throw new Error(`root changing: ${roots[i]}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await createAcpxPrivateSnapshot({ roots });\n} catch (e) {\n  if (e.message.includes('package root changed during snapshot')) {\n    await waitForInstallsToFinish();\n    await createAcpxPrivateSnapshot({ roots });\n  } else throw e;\n}","preventionTips":["Serialize installs and snapshot creation — never overlap them","Pause file-sync tools (rsync, cloud sync) over the workspace during runs","Pin dependency versions so package directories are not replaced mid-run"],"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-14T05:17:10.506Z"}