{"record":{"id":"9f908a25792c1297","repo":"deepseek-ai/deepseek-harness","slug":"sessions-provide-duplicate-hook-name","errorCode":null,"errorMessage":"sessions.provide: duplicate hook \"${name}\"","messagePattern":"sessions\\.provide: duplicate hook \"(.+?)\"","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/client/runtime/src/client/sessions/provide.ts","lineNumber":180,"sourceCode":"      // session's projection store (open key space — never a static roster member).\n      projections: { faceOf: key => binding.session.projections.faceOf(key) },\n    }\n  }\n\n  /** Rebuild the static projection and the owner's live bundles, then republish the current one. */\n  private applyRosterChange(): void {\n    this.maybeInfoCache = this.materializeMaybeInfo()\n    this.host.rebuildBundles()\n    this.publishCurrent()\n  }\n\n  /** Build the static no-session kit and reject duplicate declared names. */\n  private materializeMaybeInfo(): SessionMaybeProvideInfo {\n    const hooks: Record<string, undefined> = {}\n    const props: Record<string, undefined> = {}\n    for (const descriptor of this.providers) {\n      for (const name of descriptor.hooks ?? []) {\n        if (Object.hasOwn(hooks, name)) throw new Error(`sessions.provide: duplicate hook \"${name}\"`)\n        hooks[name] = undefined\n      }\n      for (const name of descriptor.props ?? []) {\n        if (Object.hasOwn(props, name)) throw new Error(`sessions.provide: duplicate prop \"${name}\"`)\n        props[name] = undefined\n      }\n    }\n    return { sessionId: undefined, hooks, props } // no projections face: every key reads absent without a session\n  }\n}\n","sourceCodeStart":162,"sourceCodeEnd":191,"githubUrl":"https://github.com/deepseek-ai/deepseek-harness/blob/b150a551b8d465e31e418e1b2eaf5e79bbb7d28e/packages/client/runtime/src/client/sessions/provide.ts#L162-L191","documentation":"Thrown at registration time while building the static no-session kit (SessionProvideChannel.materializeMaybeInfo): two providers on the roster declare the same hook name, or one descriptor lists the name twice. The check runs inside applyRosterChange before any live bundle rebuilds, so a failing sessions.provide() call rolls the provider off the roster and rethrows — the channel never stays on a roster it cannot materialize. The runtime's own built-in first provider already declares the hook 'session' (useSession rides it), so any plugin declaring 'session' hits this immediately.","triggerScenarios":"Calling sessions.provide with a descriptor whose hooks include a name another live provider already declared — including the reserved 'session' — or the same name twice within one descriptor. Also reachable from the channel-constructor path when a host assembles an invalid initial roster, and re-validated when a disposer shrinks the roster.","commonSituations":"Copy-pasting a provider without renaming its hooks; upgrading a plugin that starts declaring a hook another plugin already owns; test fixtures assembling provider rosters directly instead of through provide().","solutions":["Rename the colliding hook so each name has exactly one owner, namespaced like 'review.sessionState'","Remove the duplicate declaration from one descriptor's hooks array","Never declare 'session' — the built-in provider owns it and useSession depends on it"],"exampleFix":"// before — collides with the runtime's reserved 'session' hook\nsessions.provide({ hooks: ['session'], resolve: (b) => ({ hooks: { session: b.session } }) })\n\n// after — unique, namespaced hook name\nsessions.provide({\n  hooks: ['review.sessionState'],\n  resolve: (b) => ({ hooks: { 'review.sessionState': reviewStateOf(b) } }),\n})","handlingStrategy":"validation","validationCode":"// hooks variant of the roster uniqueness check\nfunction assertUniqueHooks(descriptors: SessionProvideDescriptor[]): void {\n  const seen = new Set<string>()\n  for (const descriptor of descriptors) {\n    for (const name of descriptor.hooks ?? []) {\n      if (seen.has(name)) throw new Error(`duplicate hook ${name}`)\n      seen.add(name)\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const disposer = sessions.provide(descriptor)\n} catch (error) {\n  // the channel rolled the provider off the roster and rethrew; rename the hook and retry\n}","preventionTips":["Treat 'session' as reserved — the runtime's built-in provider owns it","Namespace hook names by plugin, e.g. 'review.sessionState'","Run the roster uniqueness assertion in plugin tests before registration"],"tags":["sessions-provide","hooks","duplicate-name","registration"],"backgroundTag":"duplicate-provider-registration","analyzedSha":"b150a551b8d465e31e418e1b2eaf5e79bbb7d28e","analyzedAt":"2026-08-24T18:12:29.105Z","schemaVersion":2},"datasetVersion":"2026-08-24T22:17:12.610Z"}