deepseek-ai/deepseek-harness · error

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

Error message

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

What it means

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.

Source

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

      // 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()
  }

  /** Build the static no-session kit and reject duplicate declared names. */
  private materializeMaybeInfo(): SessionMaybeProvideInfo {
    const hooks: Record<string, undefined> = {}
    const props: Record<string, undefined> = {}
    for (const descriptor of this.providers) {
      for (const name of descriptor.hooks ?? []) {
        if (Object.hasOwn(hooks, name)) throw new Error(`sessions.provide: duplicate hook "${name}"`)
        hooks[name] = undefined
      }
      for (const name of descriptor.props ?? []) {
        if (Object.hasOwn(props, name)) throw new Error(`sessions.provide: duplicate prop "${name}"`)
        props[name] = undefined
      }
    }
    return { sessionId: undefined, hooks, props } // no projections face: every key reads absent without a session
  }
}

View on GitHub (pinned to b150a551b8)

Solutions

  1. Rename the colliding hook so each name has exactly one owner, namespaced like 'review.sessionState'
  2. Remove the duplicate declaration from one descriptor's hooks array
  3. Never declare 'session' — the built-in provider owns it and useSession depends on it

Example fix

// before — collides with the runtime's reserved 'session' hook
sessions.provide({ hooks: ['session'], resolve: (b) => ({ hooks: { session: b.session } }) })

// after — unique, namespaced hook name
sessions.provide({
  hooks: ['review.sessionState'],
  resolve: (b) => ({ hooks: { 'review.sessionState': reviewStateOf(b) } }),
})
Defensive patterns

Strategy: validation

Validate before calling

// hooks variant of the roster uniqueness check
function assertUniqueHooks(descriptors: SessionProvideDescriptor[]): void {
  const seen = new Set<string>()
  for (const descriptor of descriptors) {
    for (const name of descriptor.hooks ?? []) {
      if (seen.has(name)) throw new Error(`duplicate hook ${name}`)
      seen.add(name)
    }
  }
}

Try / catch

try {
  const disposer = sessions.provide(descriptor)
} catch (error) {
  // the channel rolled the provider off the roster and rethrew; rename the hook and retry
}

Prevention

When it happens

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

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

Related errors


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