deepseek-ai/deepseek-harness · error

sessions.provide: duplicate prop "${name}"

Error message

sessions.provide: duplicate prop "${name}"

What it means

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.

Source

Thrown at packages/client/runtime/src/client/sessions/provide.ts:153

      for (const name of Object.keys(contributedHooks)) {
        if (!(descriptor.hooks ?? []).includes(name)) {
          throw new Error(`sessions.provide: undeclared hook "${name}"`)
        }
      }
      for (const name of Object.keys(contributedProps)) {
        if (!(descriptor.props ?? []).includes(name)) {
          throw new Error(`sessions.provide: undeclared prop "${name}"`)
        }
      }
      for (const name of descriptor.hooks ?? []) {
        const source = contributedHooks[name]
        if (source === undefined) throw new Error(`sessions.provide: missing hook "${name}"`)
        if (Object.hasOwn(hooks, name)) throw new Error(`sessions.provide: duplicate hook "${name}"`)
        hooks[name] = source
      }
      for (const name of descriptor.props ?? []) {
        if (!Object.hasOwn(contributedProps, name)) throw new Error(`sessions.provide: missing prop "${name}"`)
        if (Object.hasOwn(props, name)) throw new Error(`sessions.provide: duplicate prop "${name}"`)
        props[name] = contributedProps[name]
      }
    }
    return {
      sessionId: binding.sessionId,
      hooks,
      props,
      // The useProjection seat: key-addressed bare value faces off the
      // session's projection store (open key space — never a static roster member).
      projections: { faceOf: key => binding.session.projections.faceOf(key) },
    }
  }

  /** Rebuild the static projection and the owner's live bundles, then republish the current one. */
  private applyRosterChange(): void {
    this.maybeInfoCache = this.materializeMaybeInfo()
    this.host.rebuildBundles()
    this.publishCurrent()

View on GitHub (pinned to b150a551b8)

Solutions

  1. Give each prop name a single owning provider — namespace names by domain, e.g. 'sidebar.panelOpen' versus 'workspace.panelOpen'
  2. Remove the duplicate name from one descriptor's props array and from its resolver
  3. 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

Example fix

// before — two providers both declare and contribute 'panelOpen'
sessions.provide({ props: ['panelOpen'], resolve: (b) => ({ props: { panelOpen: sidebarOpen(b) } }) })
sessions.provide({ props: ['panelOpen'], resolve: (b) => ({ props: { panelOpen: workspaceOpen(b) } }) })

// after — one name, one owner
sessions.provide({ props: ['sidebar.panelOpen'], resolve: (b) => ({ props: { 'sidebar.panelOpen': sidebarOpen(b) } }) })
sessions.provide({ props: ['workspace.panelOpen'], resolve: (b) => ({ props: { 'workspace.panelOpen': workspaceOpen(b) } }) })
Defensive patterns

Strategy: validation

Validate before calling

// reject a roster with overlapping names before any session materializes
function assertUniqueRoster(descriptors: SessionProvideDescriptor[]): void {
  const seen = new Set<string>()
  for (const descriptor of descriptors) {
    for (const name of [...(descriptor.hooks ?? []), ...(descriptor.props ?? [])]) {
      if (seen.has(name)) throw new Error(`duplicate roster member ${name}`)
      seen.add(name)
    }
  }
}

Try / catch

try {
  sessions.provide(descriptor)
} catch (error) {
  // registration rolled back automatically; rename the colliding member and re-register
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of deepseek-ai/deepseek-harness@b150a551b8 (2026-08-24). Data as JSON: /api/errors/f27293b628e2ec9a. Report an issue: GitHub.