{"record":{"id":"60d3665bd95beb26","repo":"moeru-ai/airi","slug":"extension-import-plan-is-missing-or-was-already-used","errorCode":null,"errorMessage":"Extension import plan is missing or was already used.","messagePattern":"Extension import plan is missing or was already used\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":480,"sourceCode":"    try {\n      return await operation()\n    }\n    finally {\n      stopAccessing()\n    }\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) {","sourceCodeStart":462,"sourceCodeEnd":498,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L462-L498","documentation":"commitPreparedPlan looks up the import plan by planId in an in-memory map and throws when it is absent. Plans are consumed exactly once: commit deletes the plan after a successful commit, and plans live only in process memory, so restarts or double-commits make the id invalid. This is a one-time-token state check.","triggerScenarios":"Calling commit (which routes to commitPreparedPlan) with a planId that was never prepared; calling commit twice with the same planId (second call fails because plans.delete ran on success); restarting the Electron main process between prepare and commit, clearing the in-memory plans map.","commonSituations":"Renderer retry logic re-sending a commit after a timeout when the first commit actually succeeded; user closing/restarting the app between reviewing the folder and confirming the import; corrupted or fabricated planId from the renderer.","solutions":["Call prepare (folder re-selection/preview) again to obtain a fresh planId, then commit once with it.","Track commit state in the caller so commit is invoked at most once per planId.","If the app restarted, redo the whole import flow: prepare -> review -> commit; in-memory plans do not survive restarts.","Verify the renderer is passing the planId returned by prepare unchanged."],"exampleFix":"// before\nawait commit(preparedPlanId)\nawait commit(preparedPlanId) // second call throws\n// after\nif (!committedPlanIds.has(preparedPlanId)) {\n  await commit(preparedPlanId)\n  committedPlanIds.add(preparedPlanId)\n}","handlingStrategy":"try-catch","validationCode":"if (committedPlanIds.has(planId)) {\n  return // already committed; skip duplicate call\n}","typeGuard":null,"tryCatchPattern":"try {\n  await importService.commit(planId)\n} catch (error) {\n  if (error.message.includes('missing or was already used')) {\n    // re-run prepare to get a fresh planId before committing again\n  }\n}","preventionTips":["Commit each planId exactly once; guard retry logic with a committed set.","Do not persist planIds across app restarts; redo prepare -> review -> commit after a restart.","Pass the planId returned by prepare through unchanged."],"tags":["state","one-time-token","extension-import"],"backgroundTag":"record-not-found","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"}