{"record":{"id":"dca81bf9461bd81e","repo":"moeru-ai/airi","slug":"extension-source-changed-after-review-select-the-folder","errorCode":null,"errorMessage":"Extension source changed after review. Select the folder again.","messagePattern":"Extension source changed after review\\. 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":485,"sourceCode":"    }\n  }\n\n  private async assertDestinationAvailable(extensionId: string): Promise<void> {\n    const destination = join(this.extensionsRoot, extensionId)\n    if (await this.isExtensionInstalled(extensionId) || await pathExists(destination)) {\n      throw new Error(`Extension is already installed: ${extensionId}`)\n    }\n  }\n\n  private async commitPreparedPlan(planId: string): Promise<ManifestEntry> {\n    const storedPlan = this.plans.get(planId)\n    if (!storedPlan) {\n      throw new Error('Extension import plan is missing or was already used.')\n    }\n    return await this.withSecurityScopedAccess(storedPlan.securityScopedBookmark, async () => {\n      const inspected = await inspectExtensionDirectory(storedPlan.sourcePath)\n      if (inspected.manifest.id !== storedPlan.preview.extensionId || inspected.fingerprint !== storedPlan.fingerprint) {\n        throw new Error('Extension source changed after review. Select the folder again.')\n      }\n\n      await this.assertDestinationAvailable(inspected.manifest.id)\n      await mkdir(this.extensionsRoot, { recursive: true })\n      const stagingRoot = join(this.extensionsRoot, '.imports')\n      await mkdir(stagingRoot, { recursive: true })\n      const stagingPath = join(stagingRoot, planId)\n      const destination = join(this.extensionsRoot, inspected.manifest.id)\n\n      try {\n        await copyExtensionDirectory(inspected.sourcePath, stagingPath)\n        const staged = await inspectExtensionDirectory(stagingPath)\n        if (staged.manifest.id !== inspected.manifest.id || staged.fingerprint !== inspected.fingerprint) {\n          throw new Error('Extension copy does not match the reviewed package.')\n        }\n        await this.assertDestinationAvailable(inspected.manifest.id)\n        await rename(stagingPath, destination)\n        this.plans.delete(planId)","sourceCodeStart":467,"sourceCodeEnd":503,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L467-L503","documentation":"Before committing, commitPreparedPlan re-inspects the source directory and compares the manifest id and content fingerprint against what was captured during prepare/preview. If the folder contents changed between the user's review and the commit, it throws so the user never installs something different from what they saw. The fingerprint is effectively a checksum of the reviewed package.","triggerScenarios":"Files in the selected extension folder added/removed/modified between prepare() and commit() such that inspectExtensionDirectory returns a different fingerprint, or the manifest.id changed (e.g. user swapped the folder contents or edited manifest.json after preview).","commonSituations":"User edits manifest.json while the confirm dialog is open; sync tools (iCloud/Dropbox/git checkout) modify the folder mid-review; a different folder is committed than the one previewed; build tooling rewrites the directory during the review window.","solutions":["Stop any process modifying the folder, then restart the import: select the folder again (prepare), review, and commit promptly.","Verify manifest.json id still matches the previewed extension; revert edits made after preview.","Exclude the folder from sync/build tools during import, or copy it to a stable location and import from there.","Re-run prepare to regenerate the fingerprint, then commit without delay."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// before commit, compare current folder fingerprint to the one from prepare\nconst current = await inspectExtensionDirectory(sourcePath)\nif (current.fingerprint !== preparedFingerprint) {\n  throw new Error('Folder changed since review; re-select the folder.')\n}","typeGuard":null,"tryCatchPattern":"try {\n  await importService.commit(planId)\n} catch (error) {\n  if (error.message.includes('changed after review')) {\n    // prompt the user to re-select the folder and redo prepare -> commit\n  }\n}","preventionTips":["Do not edit the extension folder (especially manifest.json) between preview and confirm.","Pause file-sync or build tools that touch the folder during import.","Commit promptly after reviewing the preview; long delays widen the race window."],"tags":["fingerprint","concurrent-modification","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"}