{"record":{"id":"91de9212365b7128","repo":"deepseek-ai/deepseek-harness","slug":"sessions-provide-missing-prop-name","errorCode":null,"errorMessage":"sessions.provide: missing prop \"${name}\"","messagePattern":"sessions\\.provide: missing prop \"(.+?)\"","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/client/runtime/src/client/sessions/provide.ts","lineNumber":152,"sourceCode":"      const contributedProps = contribution.props ?? {}\n      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()","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/deepseek-ai/deepseek-harness/blob/b150a551b8d465e31e418e1b2eaf5e79bbb7d28e/packages/client/runtime/src/client/sessions/provide.ts#L134-L170","documentation":"Thrown while materializing a session's standard-props bundle (SessionProvideChannel.materializeInfo): a provider descriptor declared a prop name in its props roster, but its resolve(binding) contribution did not include that key. The provide channel is fail-loud by design — the declared roster and the resolver's actual output must agree exactly for every session binding — so a missing member aborts materialization instead of silently yielding an undefined prop. Materialization runs from the constructor, from record when a session binding arrives, and from provideInfo when a roster change rebuilds live bundles (a failing provide() registration rolls back and rethrows).","triggerScenarios":"Registering sessions.provide({ props: ['x'], resolve }) whose resolve returns an object whose props omits 'x' — e.g. a conditional spread ...(cond ? { props: { x } } : {}), or a rename applied to the resolver but not the descriptor. The throw fires at the next materialization of any session bundle under that roster.","commonSituations":"Writing a new sessions.provide plugin and letting the declaration drift from the resolver; renaming a prop in one place during a refactor; returning hooks but forgetting props; fixture-driven test doubles that construct contributions by hand.","solutions":["Make resolve(binding) return every name listed in descriptor.props for every binding — roster members must be unconditional values","If the value genuinely may be absent, remove the name from descriptor.props (without a session every key already reads absent) or model it through the open projections key space instead of a roster member","Check for typos and casing drift between the descriptor array and the resolver's returned object keys","Add a unit test that runs the resolver against a representative binding and asserts Object.keys(contribution.props) matches descriptor.props"],"exampleFix":"// before — declared prop never contributed\nsessions.provide({\n  props: ['workspaceTitle'],\n  resolve: (binding) => ({ hooks: { session: binding.session } }),\n})\n\n// after — every declared member is returned on every binding\nsessions.provide({\n  props: ['workspaceTitle'],\n  resolve: (binding) => ({\n    props: { workspaceTitle: titleOf(binding) },\n  }),\n})","handlingStrategy":"validation","validationCode":"// registration-time smoke check: the resolver must cover the declared roster\nfunction assertResolverCoversDescriptor(\n  descriptor: SessionProvideDescriptor,\n  sample: SessionBinding,\n): void {\n  const contribution = descriptor.resolve(sample)\n  for (const name of descriptor.props ?? []) {\n    if (!Object.hasOwn(contribution.props ?? {}, name)) {\n      throw new Error(`provider would fail materialization: missing prop ${name}`)\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const disposer = sessions.provide(descriptor)\n} catch (error) {\n  // the channel already rolled the registration back; fix the descriptor/resolver mismatch, not the call site\n}","preventionTips":["Keep the descriptor roster and the resolver in one adjacent block and review them together","Never conditionally omit a declared member — declare only unconditional values","Cover each provider with a test that materializes against a representative binding","Treat any sessions.provide: message as a plugin-authoring bug, not a runtime condition to absorb"],"tags":["sessions-provide","props","provider-descriptor","fail-loud"],"backgroundTag":"missing-required-field","analyzedSha":"b150a551b8d465e31e418e1b2eaf5e79bbb7d28e","analyzedAt":"2026-08-24T18:12:29.105Z","schemaVersion":2},"datasetVersion":"2026-08-24T22:17:12.610Z"}