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