moeru-ai/airi · error · Error

Extension module ` ` is already registered for session .

Error message

Extension module `${input.id}` is already registered for session ${session.id}.

What it means

During startExtension, plugin-host gives each module a ctx.modules.register(input) that stores modules in a per-session Map keyed by input.id (packages/plugin-sdk/src/plugin-host/core.ts). Registering the same id twice within one extension session throws this error, including the session id for diagnosis. This guards the module registry from shadowing: permissions are intersected per module and modules are withdrawn by id on teardown, so duplicate ids would corrupt both.

Solutions

  1. Give each module a unique id within the extension (namespace them, e.g. 'main.ui' vs 'main.tools').
  2. Make registration idempotent on your side: keep a Set of registered ids per session and skip re-registering.
  3. If double registration comes from double setup, guard setup with an initialized flag or dispose the old session before starting a new one.
  4. If you truly need to replace a module, withdraw/dispose the old one first so the Map key frees up.

Example fix

// before
await ctx.modules.register({ id: 'ui', ... })
await ctx.modules.register({ id: 'ui', ... }) // throws: already registered

// after
const registered = new Set<string>()
async function registerOnce(input) {
  if (registered.has(input.id)) return
  registered.add(input.id)
  await ctx.modules.register(input)
}
Defensive patterns

Strategy: validation

Validate before calling

const registeredIds = new Set<string>()
async function registerModule(ctx: ExtensionSetupContext, input: RegisterExtensionModuleInput) {
  if (registeredIds.has(input.id)) return // idempotent per session
  registeredIds.add(input.id)
  await ctx.modules.register(input)
}

Try / catch

try {
  await ctx.modules.register(input)
} catch (error) {
  if (errorMessageFrom(error).includes('is already registered for session')) {
    return // double setup: previous registration already owns the id
  }
  throw error
}

Prevention

When it happens

Trigger: An extension entrypoint whose setup registers two modules with the same id (copy-paste); a module that re-registers itself on reconnect/retry inside the same session; frameworks that mount a module's init twice (StrictMode-like double effects) against one session; id derived from a variable that collapses to the same string.

Common situations: Copy-pasted module registrations during development; retry logic around register that ignores that the first call already succeeded; React/Vue double-mount lifecycles reusing a cached session.

Related errors


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

Appendix: source

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

      modules: new Map(),
      permissions: {
        requested: permissionSnapshot.requested,
        granted: permissionSnapshot.granted,
        revision: permissionSnapshot.revision,
      },
      subscriptions,
    }

    this.extensionSessionService.register(session)

    const ctx: ExtensionSetupContext = {
      extension: session.extension,
      kits: this.createExtensionKitRegistry(session),
      subscriptions,
      modules: {
        register: async (input: RegisterExtensionModuleInput) => {
          if (session.modules.has(input.id)) {
            throw new Error(`Extension module \`${input.id}\` is already registered for session ${session.id}.`)
          }

          const moduleSubscriptions = new DisposableStore()
          const permissions = this.permissions.intersectGrant(
            session.permissions.granted,
            input.permissions ?? session.permissions.granted,
          )
          const module: ExtensionModuleContext = {
            id: input.id,
            identity: {
              id: input.id,
              extension: session.extension,
              labels: input.labels,
            },
            permissions,
            kits: this.createModuleKitRegistry(session, moduleSubscriptions, input.id),
            subscriptions: moduleSubscriptions,
            dispose: async () => {

View on GitHub (pinned to 438a067dde)