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

  1. Re-select the extension folder once it is quiescent so a fresh consistent copy is taken
  2. Stop build watchers, editors, and cloud-sync clients touching the folder, then retry the import
  3. 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
  4. 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

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


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)