{"record":{"id":"9e3c457cc83982a1","repo":"moeru-ai/airi","slug":"extension-module-input-id-is-already-register","errorCode":null,"errorMessage":"Extension module `${input.id}` is already registered for session ${session.id}.","messagePattern":"Extension module `(.+?)` is already registered for session (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/plugin-sdk/src/plugin-host/core.ts","lineNumber":309,"sourceCode":"      modules: new Map(),\n      permissions: {\n        requested: permissionSnapshot.requested,\n        granted: permissionSnapshot.granted,\n        revision: permissionSnapshot.revision,\n      },\n      subscriptions,\n    }\n\n    this.extensionSessionService.register(session)\n\n    const ctx: ExtensionSetupContext = {\n      extension: session.extension,\n      kits: this.createExtensionKitRegistry(session),\n      subscriptions,\n      modules: {\n        register: async (input: RegisterExtensionModuleInput) => {\n          if (session.modules.has(input.id)) {\n            throw new Error(`Extension module \\`${input.id}\\` is already registered for session ${session.id}.`)\n          }\n\n          const moduleSubscriptions = new DisposableStore()\n          const permissions = this.permissions.intersectGrant(\n            session.permissions.granted,\n            input.permissions ?? session.permissions.granted,\n          )\n          const module: ExtensionModuleContext = {\n            id: input.id,\n            identity: {\n              id: input.id,\n              extension: session.extension,\n              labels: input.labels,\n            },\n            permissions,\n            kits: this.createModuleKitRegistry(session, moduleSubscriptions, input.id),\n            subscriptions: moduleSubscriptions,\n            dispose: async () => {","sourceCodeStart":291,"sourceCodeEnd":327,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/plugin-sdk/src/plugin-host/core.ts#L291-L327","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait ctx.modules.register({ id: 'ui', ... })\nawait ctx.modules.register({ id: 'ui', ... }) // throws: already registered\n\n// after\nconst registered = new Set<string>()\nasync function registerOnce(input) {\n  if (registered.has(input.id)) return\n  registered.add(input.id)\n  await ctx.modules.register(input)\n}","handlingStrategy":"validation","validationCode":"const registeredIds = new Set<string>()\nasync function registerModule(ctx: ExtensionSetupContext, input: RegisterExtensionModuleInput) {\n  if (registeredIds.has(input.id)) return // idempotent per session\n  registeredIds.add(input.id)\n  await ctx.modules.register(input)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await ctx.modules.register(input)\n} catch (error) {\n  if (errorMessageFrom(error).includes('is already registered for session')) {\n    return // double setup: previous registration already owns the id\n  }\n  throw error\n}","preventionTips":["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."],"tags":["plugin-sdk","extension","modules","registration","lifecycle"],"backgroundTag":"duplicate-registration","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}