paperclipai/paperclip · error · Error

ACPX package root changed during snapshot

Error message

ACPX package root changed during snapshot

What it means

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.

Source

Thrown at packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts:153

      ) {
        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");
      }
    }
    // Supply bare-package lookup links only for already admitted package roots.
    const packages: Array<{ name: string; root: string }> = [];
    for (const root of roots) {
      const file = await open(join(root, "package.json")).catch(() => null);
      if (!file) continue;
      try {
        const metadata = JSON.parse(await file.readFile("utf8"));
        if (
          typeof metadata.name === "string" &&
          /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(metadata.name)
        ) {
          packages.push({ name: metadata.name, root });
        }
      } finally {
        await file.close();
      }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Run the snapshot only after package installation has fully completed and the tree is idle
  2. Retry the snapshot — the identity check is designed so a quiet re-run succeeds
  3. Exclude the package root from background sync/scan tools during runs
  4. Lock dependency installs so no concurrent process replaces package directories

Example fix

// before
spawn('npm', ['install']);
await snapshot(roots);
// after
execSync('npm install');
await snapshot(roots);
Defensive patterns

Strategy: retry

Validate before calling

const ids = [];
for (const root of roots) ids.push(await fs.stat(root));
await new Promise(r => setTimeout(r, 300));
for (let i = 0; i < roots.length; i++) {
  const now = await fs.stat(roots[i]);
  if (now.ino !== ids[i].ino || now.mtimeMs !== ids[i].mtimeMs) throw new Error(`root changing: ${roots[i]}`);
}

Try / catch

try {
  await createAcpxPrivateSnapshot({ roots });
} catch (e) {
  if (e.message.includes('package root changed during snapshot')) {
    await waitForInstallsToFinish();
    await createAcpxPrivateSnapshot({ roots });
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: `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.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/6726fda41caf7a54. Report an issue: GitHub.