{"record":{"id":"4559bcd8b8cd93e6","repo":"paperclipai/paperclip","slug":"acpx-file-ended-during-snapshot","errorCode":null,"errorMessage":"ACPX file ended during snapshot","messagePattern":"ACPX file ended during snapshot","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts","lineNumber":46,"sourceCode":"  digests: Record<string, string>;\n  handoff: { path: string; digest: string };\n  close(): Promise<void>;\n}\nconst digest = (bytes: Buffer) =>\n  createHash(\"sha256\").update(bytes).digest(\"hex\");\nconst within = (root: string, file: string) => {\n  const rel = relative(root, file);\n  return (\n    rel === \"\" || (rel !== \"..\" && !rel.startsWith(\"../\") && !isAbsolute(rel))\n  );\n};\n\nasync function readSnapshotBytes(handle: FileHandle, byteLength: number): Promise<Buffer> {\n  const bytes = Buffer.alloc(byteLength);\n  let offset = 0;\n  while (offset < bytes.length) {\n    const read = await handle.read(bytes, offset, bytes.length - offset, offset);\n    if (!read.bytesRead) throw new Error(\"ACPX file ended during snapshot\");\n    offset += read.bytesRead;\n  }\n  return bytes;\n}\n\n/** macOS has no /proc directory descriptors. Freeze only the admitted package roots. */\nexport async function createAcpxPrivateSnapshot(\n  sourceRoots: readonly string[],\n  executable: FileHandle | null,\n): Promise<AcpxPrivateSnapshot> {\n  sourceRoots = await Promise.all(sourceRoots.map((root) => realpath(root)));\n  const sourceIdentities = await Promise.all(\n    sourceRoots.map((root) => lstat(root, { bigint: true })),\n  );\n  const directory = await realpath(\n    await mkdtemp(join(tmpdir(), \"paperclip-acpx-\")),\n  );\n  const roots = sourceRoots.map((_, index) => join(directory, String(index)));","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts#L28-L64","documentation":"readSnapshotBytes fills a pre-allocated Buffer of exactly byteLength bytes from an open file handle for an ACPX package snapshot. It loops until the buffer is full; if a read returns 0 bytes (EOF reached before the expected length), it throws. The library treats a truncated file as a snapshot integrity failure because the stat-reported size no longer matches the file's readable content.","triggerScenarios":"Calling createAcpxPrivateSnapshot (via privateSnapshot/snapshot) on a package file whose size shrinks or is truncated between the lstat/handle.stat size check and the sequential reads inside readSnapshotBytes — i.e. read.bytesRead becomes 0 at some offset.","commonSituations":"A concurrent process rewrites/truncates a node_modules file during snapshot (e.g. pnpm/npm install running at the same time); a sparse or network-mounted file whose stat size exceeds readable bytes; a stale build artifact partially deleted by a watcher/cleaner.","solutions":["Ensure no package manager or build watcher mutates the package tree while snapshotting; quiesce installs first","Re-run the snapshot — the error is usually a transient race, not a corrupted package","Verify the file is locally readable and fully written (check on-disk size vs stat size; avoid snapshotting network mounts)","Pin/lock dependencies so package directories are not replaced mid-run"],"exampleFix":"// before: snapshot taken while install runs\nawait exec('npm install &');\nawait snapshot(roots);\n// after: quiesce installs before snapshot\nawait exec('npm install');\nawait snapshot(roots);","handlingStrategy":"retry","validationCode":"const st = await fs.stat(file);\nif (!st.isFile()) throw new Error('not a regular file');\n// readable size sanity check\nconst fd = await fs.open(file, 'r');\nconst buf = Buffer.alloc(1);\nawait fd.read(buf, 0, 1, Math.max(0, st.size - 1));","typeGuard":null,"tryCatchPattern":"try {\n  await snapshot(roots);\n} catch (e) {\n  if (e.message.includes('ended during snapshot')) {\n    await waitForTreeIdle(roots);\n    await snapshot(roots); // retry once after quiescing writers\n  } else throw e;\n}","preventionTips":["Never run package installs concurrently with session snapshots","Exclude log/cache directories from admitted package roots","Avoid snapshotting files on network mounts where stat size can exceed readable bytes"],"tags":["filesystem","race-condition","snapshot"],"backgroundTag":"file-read-failed","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"}