moeru-ai/airi · error · Error

Module ` ` was not found.

Error message

Module `${moduleId}` was not found.

What it means

getModuleOrThrow (core.ts) looks a module up in the host's module registry by moduleId and throws this error when no such module exists. Modules enter the registry only via ctx.modules.register during an extension session and leave it on withdraw/unbind — stopExtension iterates owned modules and withdraws them all. So the error means either the id was never registered, or the owning extension's session already cleaned the module up.

Solutions

  1. Verify the module id against what the extension actually registers (ctx.modules.register({ id, ... })) — copy the exact string.
  2. If the owning extension was stopped/reloaded, re-establish its session and wait for module registration before invoking module-scoped APIs.
  3. List current modules from the registry (the host exposes listByOwner-style queries) instead of caching ids.
  4. On the caller side, catch this and refresh module listings rather than retrying the same dead id.

Example fix

// before
await host.callModule(sessionId, 'mocap-ui', input) // module id is 'ui.mocap'

// after
const modules = await host.listModulesFor(sessionId)
const target = modules.find(m => m.moduleId.endsWith('mocap'))
if (!target) throw new Error('mocap module not registered yet')
await host.callModule(sessionId, target.moduleId, input)
Defensive patterns

Strategy: validation

Validate before calling

const modules = host.modules.listByOwner(sessionId) // or registry listing API
const target = modules.find(m => m.moduleId === moduleId)
if (!target) {
  // module withdrawn or not yet registered: refresh and wait instead of invoking
}
await host.invokeModule(sessionId, moduleId, input)

Try / catch

try {
  await host.callModule(sessionId, moduleId, input)
} catch (error) {
  if (errorMessageFrom(error).includes('` was not found.')) {
    // stale module id (extension restarted): re-resolve from the live registry
    return refreshModulesAndRetry(sessionId, input)
  }
  throw error
}

Prevention

When it happens

Trigger: Passing a moduleId string that was never registered (typo, renamed module); calling a host API that takes (sessionId, moduleId) after the extension stopped and its modules were withdrawn; referencing a module registered under a different session; races where a call arrives after module disposal.

Common situations: Host-side orchestration code holding module ids across extension restarts; UI dropdowns listing modules from a stale snapshot; renaming a module id in the extension without updating host-side callers.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-08-18). Data as JSON: /api/errors/6b3b55ee9a9e7c0f. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugin-sdk/src/plugin-host/core.ts:583

    contribution.install(this.installContext)
  }

  private async cleanupExtensionSession(session: ExtensionSession) {
    session.phase = 'stopped'

    for (const module of this.modules.listByOwner(session.id)) {
      this.modules.withdraw(session.id, session.extension.id, module.moduleId)
      this.modules.unbind(session.id, session.extension.id, module.moduleId)
    }
    await this.cleanupExtensionSessionModules(session)
    await session.subscriptions.dispose()
    this.extensionSessionService.remove(session.id)
  }

  private getModuleOrThrow(moduleId: string) {
    const module = this.modules.get(moduleId)
    if (!module) {
      throw new Error(`Module \`${moduleId}\` was not found.`)
    }

    return module
  }

  private assertKitAvailableForRuntime(kitId: string, runtime: PluginRuntime) {
    const kit = this.kits.get(kitId)
    if (!kit) {
      throw new Error(`Kit \`${kitId}\` is not registered.`)
    }

    if (!kit.runtimes.includes(runtime)) {
      throw new Error(`Kit \`${kitId}\` is not available for runtime \`${runtime}\`.`)
    }

    return kit
  }

View on GitHub (pinned to 438a067dde)