moeru-ai/airi · error

Extension directory importer is disposed.

Error message

Extension directory importer is disposed.

What it means

The ExtensionDirectoryImporter service has a lifecycle: after dispose() is called it must not be used again. assertActive() is a guard invoked on every public operation (initialize, prepare, and paths leading to commit) that throws this error if this.disposed is true. It prevents reads/writes to a torn-down importer whose resources (queues, security-scoped access) may already be released.

Solutions

  1. Do not call initialize/prepare/commit after dispose(); check your app lifecycle so teardown happens last
  2. Guard call sites: only invoke importer methods while the owning component/window is mounted
  3. Re-create the importer instance if it was disposed and you need to import again
  4. If a pending operation should survive dispose, await this.initialization/this.commitQueue before disposing instead of racing them

Example fix

// before
// importer.dispose()
// await importer.prepare(bookmark) // throws 'disposed'
// after
// if (!importer.isDisposed) {
//   await importer.prepare(bookmark)
// }
// or recreate: const importer = createExtensionDirectoryImporter(); await importer.prepare(bookmark)
Defensive patterns

Strategy: try-catch

Validate before calling

// Track disposal in the caller and refuse calls after teardown
let disposed = false
function disposeImporter() {
  disposed = true
  importer.dispose()
}
function prepareSafe(bookmark: string) {
  if (disposed) return Promise.reject(new Error('Importer already disposed'))
  return importer.prepare(bookmark)
}

Type guard

function canUseImporter(importer: { disposed: boolean } | null | undefined): importer is { disposed: false } {
  return importer != null && !importer.disposed
}

Try / catch

try {
  await importer.initialize()
  await importer.prepare(bookmark)
}
catch (err) {
  if (errorMessageFrom(err)?.includes('importer is disposed')) {
    // recreate a fresh instance instead of retrying the disposed one
    importer = createExtensionDirectoryImporter()
    await importer.initialize()
  }
  else throw err
}

Prevention

When it happens

Trigger: Calling initialize() or prepare() (or any other guarded method) on the importer after dispose() has completed; the error is thrown synchronously by assertActive at directory-import.ts:446. Common flow: a window/page closes and disposes the importer, but a pending UI action still calls prepare().

Common situations: Double-invocation of a close/cleanup handler that disposes the importer twice; a Vue component unmount disposing the service while an in-flight dialog callback later calls prepare(); hot module reload recreating state but stale references calling the old disposed instance; calling the importer from a stale IPC handler after teardown.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

    return await result
  }

  /** Cancels one prepared plan without changing the managed Extension folder. */
  cancel(planId: string): void {
    this.plans.delete(planId)
  }

  /** Stops new work and waits for queued imports during host shutdown. */
  async dispose(): Promise<void> {
    this.disposed = true
    this.plans.clear()
    await this.initialization
    await this.commitQueue
  }

  private assertActive(): void {
    if (this.disposed) {
      throw new Error('Extension directory importer is disposed.')
    }
  }

  private async withSecurityScopedAccess<TResult>(
    bookmark: string | undefined,
    operation: () => Promise<TResult>,
  ): Promise<TResult> {
    if (!bookmark) {
      return await operation()
    }
    if (!this.startAccessingSecurityScopedResource) {
      throw new Error('Security-scoped Extension import access is not configured.')
    }

    const stopAccessing = this.startAccessingSecurityScopedResource(bookmark)
    try {
      return await operation()
    }

View on GitHub (pinned to 438a067dde)