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
- Give each module a unique id within the extension (namespace them, e.g. 'main.ui' vs 'main.tools').
- Make registration idempotent on your side: keep a Set of registered ids per session and skip re-registering.
- If double registration comes from double setup, guard setup with an initialized flag or dispose the old session before starting a new one.
- 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
- Keep a per-session Set of module ids and skip duplicates instead of relying on the throw.
- Guard double-invoked setup (framework double-mount, retry loops) with an initialized flag tied to the session.
- To replace a module, dispose/withdraw the old registration first so the id frees within the same session.
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
- Module ` ` was not found.
- Unknown extension session
- Extension entrypoint id
- gameletKit requires a host gamelet orchestration runtime.
- Module ` ` was not found.
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)