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
- Do not call initialize/prepare/commit after dispose(); check your app lifecycle so teardown happens last
- Guard call sites: only invoke importer methods while the owning component/window is mounted
- Re-create the importer instance if it was disposed and you need to import again
- 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
- Dispose the importer only in a single teardown path (e.g. onUnmounted) to avoid double-dispose races
- Guard UI callbacks so they cannot invoke the importer after unmount or window close
- Recreate the importer instance whenever a disposed one is needed again
- Ensure teardown awaits pending initialization/commit work before disposing
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
- 2. Recognition already running - this is normal if called…
- Chat session was removed before send completed
- Computer use has stopped.
- Computer use has stopped.
- Extension copy does not match the reviewed package.
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)