{"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":288,"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":270,"sourceCodeEnd":306,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/plugin-sdk/src/plugin-host/core.ts#L270-L306","documentation":"Thrown inside the modules.register callback of ExtensionSetupContext when ctx.modules.register({ id }) is called twice with the same id within a single extension session. Each session owns a Map of modules keyed by id, and duplicate registration would overwrite the prior module's subscriptions and permissions.","triggerScenarios":"Calling ctx.modules.register({ id: 'm1' }) more than once during a single extension.setup(ctx) invocation, or calling it again in a reload that reuses state incorrectly. The check is session-scoped: session.modules.has(input.id).","commonSituations":"A setup function loops over contributions and registers a module per contribution but two contributions share the same id. Or a developer calls register conditionally in two branches that both execute. Reloading an extension that did not fully dispose its prior modules can also surface this.","solutions":["Use unique module ids for each ctx.modules.register call; derive ids from contribution keys if needed.","Guard with session state: track which ids have been registered and skip or dispose-then-register if a repeat is intentional.","If re-registration is expected, dispose the existing module first (the module context exposes a dispose method)."],"exampleFix":"// before\nfor (const c of contributions) {\n  ctx.modules.register({ id: 'widget', ... }) // duplicate 'widget' on second iteration\n}\n\n// after\nfor (const c of contributions) {\n  ctx.modules.register({ id: `widget-${c.key}`, ... })\n}","handlingStrategy":"validation","validationCode":"// Inside setup(ctx):\nif (sessionTrackedModuleIds.has(input.id)) {\n  throw new Error(`Module '${input.id}' already registered; choose a unique id.`)\n}\nsessionTrackedModuleIds.add(input.id)\nawait ctx.modules.register(input)","typeGuard":"function isModuleIdUnique(alreadyRegistered: Set<string>, id: string): boolean {\n  return !alreadyRegistered.has(id)\n}","tryCatchPattern":"try {\n  await ctx.modules.register(input)\n} catch (error) {\n  if (error instanceof Error && /already registered for session/.test(error.message)) {\n    // dispose existing module first, then re-register if intentional\n  } else {\n    throw error\n  }\n}","preventionTips":["Derive module ids from stable contribution keys so duplicates surface immediately.","Track registered ids in a Set during setup and assert uniqueness before calling register.","If re-registration is intended, dispose the prior module context first."],"tags":["extension-lifecycle","module-registration","duplicate","session"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}