moeru-ai/airi · error
Extension package changed during copy. Select the folder…
Error message
Extension package changed during copy. Select the folder again.
What it means
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.
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
Example fix
// before: importing a live dev folder while `vite build --watch` runs // -> file rewritten mid-copy -> 'Extension package changed during copy' // after: snapshot then import // cp -R my-extension my-extension-stable && import ./my-extension-stable
Defensive patterns
Strategy: retry
Validate before calling
// Snapshot the directory to a stable location so sizes cannot change mid-copy // cp -R <selected-folder> <stable-path> && importer.prepare(<stable-path>) const stable = await snapshotDirectory(selectedPath) // copy to temp dir first await importer.prepare(stable)
Type guard
null
Try / catch
try {
await importer.prepare(bookmark)
}
catch (err) {
if (errorMessageFrom(err)?.includes('changed during copy')) {
// retry once against a stable snapshot of the folder
const stable = await snapshotDirectory(selectedPath)
await importer.prepare(stable)
}
else throw err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Extension copy does not match the reviewed package.
- Extension is already installed
- Extension package changed during inspection. Select the…
- does not exist
- Chat session was removed before send completed
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/68c01944d49951ef.
Report an issue: GitHub.
Appendix: source
Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:356
createReadStream(sourcePath),
async function* enforceCopyLimits(chunks) {
for await (const chunk of chunks) {
const contents = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
copiedBytes += contents.byteLength
totalBytes += contents.byteLength
if (totalBytes > extensionPackageLimits.totalBytes) {
throw new Error('Extension package exceeds the 512 MiB size limit.')
}
if (isManifest && copiedBytes > extensionPackageLimits.manifestBytes) {
throw new Error('Extension manifest exceeds the 1 MiB size limit.')
}
yield contents
}
},
createWriteStream(destinationPath, { flags: 'wx' }),
)
if (copiedBytes !== stats.size) {
throw new Error('Extension package changed during copy. Select the folder again.')
}
// Stats.mode also contains file-type bits. Only permission bits belong
// in chmod, which keeps executable entrypoints and bundled tools usable.
await chmod(destinationPath, stats.mode & 0o777)
}
}
}
/**
* Owns the prepare, review, and atomic commit lifecycle for folder imports.
*
* Prepare reads package data but never executes Extension code. Commit repeats
* validation and publishes one immutable copy under the managed registry root.
*/
export class ExtensionDirectoryImporter {
private readonly plans = new Map<string, StoredImportPlan>()
private initialization: Promise<void> | undefined
private commitQueue: Promise<void> = Promise.resolve()View on GitHub (pinned to 438a067dde)