{"record":{"id":"68c01944d49951ef","repo":"moeru-ai/airi","slug":"extension-package-changed-during-copy-select-the-folder","errorCode":null,"errorMessage":"Extension package changed during copy. Select the folder again.","messagePattern":"Extension package changed during copy\\. Select the folder again\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":356,"sourceCode":"        createReadStream(sourcePath),\n        async function* enforceCopyLimits(chunks) {\n          for await (const chunk of chunks) {\n            const contents = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n            copiedBytes += contents.byteLength\n            totalBytes += contents.byteLength\n            if (totalBytes > extensionPackageLimits.totalBytes) {\n              throw new Error('Extension package exceeds the 512 MiB size limit.')\n            }\n            if (isManifest && copiedBytes > extensionPackageLimits.manifestBytes) {\n              throw new Error('Extension manifest exceeds the 1 MiB size limit.')\n            }\n            yield contents\n          }\n        },\n        createWriteStream(destinationPath, { flags: 'wx' }),\n      )\n      if (copiedBytes !== stats.size) {\n        throw new Error('Extension package changed during copy. Select the folder again.')\n      }\n      // Stats.mode also contains file-type bits. Only permission bits belong\n      // in chmod, which keeps executable entrypoints and bundled tools usable.\n      await chmod(destinationPath, stats.mode & 0o777)\n    }\n  }\n}\n\n/**\n * Owns the prepare, review, and atomic commit lifecycle for folder imports.\n *\n * Prepare reads package data but never executes Extension code. Commit repeats\n * validation and publishes one immutable copy under the managed registry root.\n */\nexport class ExtensionDirectoryImporter {\n  private readonly plans = new Map<string, StoredImportPlan>()\n  private initialization: Promise<void> | undefined\n  private commitQueue: Promise<void> = Promise.resolve()","sourceCodeStart":338,"sourceCodeEnd":374,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L338-L374","documentation":"After streaming each file, copyExtensionDirectory compares the bytes actually written (copiedBytes accumulated in the generator) against the size recorded at fs.stat time (stats.size). A mismatch means the source file changed — was appended to, truncated, or rewritten — while it was being copied, so the staged copy may be corrupt or torn. The importer refuses to commit such a file and asks the user to re-select the folder so a consistent snapshot can be taken.","triggerScenarios":"copyExtensionDirectory (via commitPreparedPlan) finishes piping a file through the enforceCopyLimits generator; the post-copy integrity check `copiedBytes !== stats.size` at directory-import.ts:356 is true, so it throws. Any file in the package whose size changed between stat and end-of-copy triggers this.","commonSituations":"A build watcher (vite/tsc --watch) rewriting output files while the user clicks import; cloud-sync clients (Dropbox, iCloud, OneDrive) syncing the folder mid-copy; another process appending logs or caches into the selected directory; the user editing files after selecting the folder but before the copy queued behind earlier work (initialization/commitQueue) completes.","solutions":["Re-select the extension folder once it is quiescent so a fresh consistent copy is taken","Stop build watchers, editors, and cloud-sync clients touching the folder, then retry the import","Import a stable, packaged snapshot of the extension (e.g. copy the folder to a clean location first, or use a zip export) instead of a live working tree","Verify no other process is writing to the directory (lsof/fswatch) if it reproduces"],"exampleFix":"// before: importing a live dev folder while `vite build --watch` runs\n// -> file rewritten mid-copy -> 'Extension package changed during copy'\n// after: snapshot then import\n// cp -R my-extension my-extension-stable && import ./my-extension-stable","handlingStrategy":"retry","validationCode":"// Snapshot the directory to a stable location so sizes cannot change mid-copy\n// cp -R <selected-folder> <stable-path> && importer.prepare(<stable-path>)\nconst stable = await snapshotDirectory(selectedPath) // copy to temp dir first\nawait importer.prepare(stable)","typeGuard":"null","tryCatchPattern":"try {\n  await importer.prepare(bookmark)\n}\ncatch (err) {\n  if (errorMessageFrom(err)?.includes('changed during copy')) {\n    // retry once against a stable snapshot of the folder\n    const stable = await snapshotDirectory(selectedPath)\n    await importer.prepare(stable)\n  }\n  else throw err\n}","preventionTips":["Do not edit, build, or sync files in the folder while the import dialog is open","Import packaged/exported snapshots instead of live dev output directories","If the error repeats, check for background writers with lsof or a filesystem watcher"],"tags":["filesystem","race-condition","integrity","extension-import"],"backgroundTag":"checksum-mismatch","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}