{"record":{"id":"f27293b628e2ec9a","repo":"deepseek-ai/deepseek-harness","slug":"sessions-provide-duplicate-prop-name","errorCode":null,"errorMessage":"sessions.provide: duplicate prop \"${name}\"","messagePattern":"sessions\\.provide: duplicate prop \"(.+?)\"","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/client/runtime/src/client/sessions/provide.ts","lineNumber":153,"sourceCode":"      for (const name of Object.keys(contributedHooks)) {\n        if (!(descriptor.hooks ?? []).includes(name)) {\n          throw new Error(`sessions.provide: undeclared hook \"${name}\"`)\n        }\n      }\n      for (const name of Object.keys(contributedProps)) {\n        if (!(descriptor.props ?? []).includes(name)) {\n          throw new Error(`sessions.provide: undeclared prop \"${name}\"`)\n        }\n      }\n      for (const name of descriptor.hooks ?? []) {\n        const source = contributedHooks[name]\n        if (source === undefined) throw new Error(`sessions.provide: missing hook \"${name}\"`)\n        if (Object.hasOwn(hooks, name)) throw new Error(`sessions.provide: duplicate hook \"${name}\"`)\n        hooks[name] = source\n      }\n      for (const name of descriptor.props ?? []) {\n        if (!Object.hasOwn(contributedProps, name)) throw new Error(`sessions.provide: missing prop \"${name}\"`)\n        if (Object.hasOwn(props, name)) throw new Error(`sessions.provide: duplicate prop \"${name}\"`)\n        props[name] = contributedProps[name]\n      }\n    }\n    return {\n      sessionId: binding.sessionId,\n      hooks,\n      props,\n      // The useProjection seat: key-addressed bare value faces off the\n      // 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()","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/deepseek-ai/deepseek-harness/blob/b150a551b8d465e31e418e1b2eaf5e79bbb7d28e/packages/client/runtime/src/client/sessions/provide.ts#L135-L171","documentation":"Thrown while materializing a session's standard-props bundle: two providers on the roster both contributed a value under the same prop name, so the second write into the shared props record aborts materialization. The channel assigns every hook/prop name exactly one owning provider. The declared-name variant of this conflict is normally rejected earlier at registration by the static no-session check (materializeMaybeInfo, which applyRosterChange runs before rebuilding bundles), so this per-session variant surfaces when the duplicate first becomes visible in contributed values during materializeInfo — the constructor, record, and provideInfo paths, and fixture-driven test rosters that push bindings directly.","triggerScenarios":"Two plugins whose descriptors both list a prop name and whose resolvers both return it for a session binding; a roster state reached through a path that bypassed the registration-order check (e.g. a test double recording a binding under a changed roster). Each contribution must be declared in its own descriptor or the undeclared-prop error fires instead.","commonSituations":"Copy-pasting a provider and forgetting to rename its props; two plugins claiming a generic name like 'title' or 'status'; a version bump where a plugin gained a prop another plugin already owned.","solutions":["Give each prop name a single owning provider — namespace names by domain, e.g. 'sidebar.panelOpen' versus 'workspace.panelOpen'","Remove the duplicate name from one descriptor's props array and from its resolver","If both values must coexist, expose the second under a different name or through the projections key space, which is open and not roster-governed"],"exampleFix":"// before — two providers both declare and contribute 'panelOpen'\nsessions.provide({ props: ['panelOpen'], resolve: (b) => ({ props: { panelOpen: sidebarOpen(b) } }) })\nsessions.provide({ props: ['panelOpen'], resolve: (b) => ({ props: { panelOpen: workspaceOpen(b) } }) })\n\n// after — one name, one owner\nsessions.provide({ props: ['sidebar.panelOpen'], resolve: (b) => ({ props: { 'sidebar.panelOpen': sidebarOpen(b) } }) })\nsessions.provide({ props: ['workspace.panelOpen'], resolve: (b) => ({ props: { 'workspace.panelOpen': workspaceOpen(b) } }) })","handlingStrategy":"validation","validationCode":"// reject a roster with overlapping names before any session materializes\nfunction assertUniqueRoster(descriptors: SessionProvideDescriptor[]): void {\n  const seen = new Set<string>()\n  for (const descriptor of descriptors) {\n    for (const name of [...(descriptor.hooks ?? []), ...(descriptor.props ?? [])]) {\n      if (seen.has(name)) throw new Error(`duplicate roster member ${name}`)\n      seen.add(name)\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  sessions.provide(descriptor)\n} catch (error) {\n  // registration rolled back automatically; rename the colliding member and re-register\n}","preventionTips":["Namespace provider members by plugin or domain, mirroring the slot naming convention","Grep existing provide() registrations for a name before claiming it","Register providers in one assembly point so the whole roster is reviewable at a glance"],"tags":["sessions-provide","props","duplicate-name","provider-roster"],"backgroundTag":"duplicate-provider-registration","analyzedSha":"b150a551b8d465e31e418e1b2eaf5e79bbb7d28e","analyzedAt":"2026-08-24T18:12:29.105Z","schemaVersion":2},"datasetVersion":"2026-08-24T22:17:12.610Z"}