moeru-ai/airi · error

Extension import plan is missing or was already used.

Error message

Extension import plan is missing or was already used.

What it means

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.

Solutions

  1. Call prepare (folder re-selection/preview) again to obtain a fresh planId, then commit once with it.
  2. Track commit state in the caller so commit is invoked at most once per planId.
  3. If the app restarted, redo the whole import flow: prepare -> review -> commit; in-memory plans do not survive restarts.
  4. Verify the renderer is passing the planId returned by prepare unchanged.

Example fix

// before
await commit(preparedPlanId)
await commit(preparedPlanId) // second call throws
// after
if (!committedPlanIds.has(preparedPlanId)) {
  await commit(preparedPlanId)
  committedPlanIds.add(preparedPlanId)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (committedPlanIds.has(planId)) {
  return // already committed; skip duplicate call
}

Try / catch

try {
  await importService.commit(planId)
} catch (error) {
  if (error.message.includes('missing or was already used')) {
    // re-run prepare to get a fresh planId before committing again
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17). Data as JSON: /api/errors/60d3665bd95beb26. Report an issue: GitHub.

Appendix: source

Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:480

    try {
      return await operation()
    }
    finally {
      stopAccessing()
    }
  }

  private async assertDestinationAvailable(extensionId: string): Promise<void> {
    const destination = join(this.extensionsRoot, extensionId)
    if (await this.isExtensionInstalled(extensionId) || await pathExists(destination)) {
      throw new Error(`Extension is already installed: ${extensionId}`)
    }
  }

  private async commitPreparedPlan(planId: string): Promise<ManifestEntry> {
    const storedPlan = this.plans.get(planId)
    if (!storedPlan) {
      throw new Error('Extension import plan is missing or was already used.')
    }
    return await this.withSecurityScopedAccess(storedPlan.securityScopedBookmark, async () => {
      const inspected = await inspectExtensionDirectory(storedPlan.sourcePath)
      if (inspected.manifest.id !== storedPlan.preview.extensionId || inspected.fingerprint !== storedPlan.fingerprint) {
        throw new Error('Extension source changed after review. Select the folder again.')
      }

      await this.assertDestinationAvailable(inspected.manifest.id)
      await mkdir(this.extensionsRoot, { recursive: true })
      const stagingRoot = join(this.extensionsRoot, '.imports')
      await mkdir(stagingRoot, { recursive: true })
      const stagingPath = join(stagingRoot, planId)
      const destination = join(this.extensionsRoot, inspected.manifest.id)

      try {
        await copyExtensionDirectory(inspected.sourcePath, stagingPath)
        const staged = await inspectExtensionDirectory(stagingPath)
        if (staged.manifest.id !== inspected.manifest.id || staged.fingerprint !== inspected.fingerprint) {

View on GitHub (pinned to 438a067dde)