deepseek-ai/deepseek-harness · error

ctx-level renderSlot only renders 'root' (got "${key}"); chi

Error message

ctx-level renderSlot only renders 'root' (got "${key}"); child slots render through the component props face

What it means

The context-level slot renderer (ctx.slots.renderSlot) exists only for the shell to render the 'root' slot; every nested slot renders through the component props face (the render-slots share handed to a registered component). The guard rejects any non-root key at runtime. It exists because this package's own TypeScript program narrows SlotMap to just 'root' — making the check constant-false and elidable there — while plain-JavaScript or cross-program callers can pass a wider K and must still be stopped.

Source

Thrown at packages/client/runtime/src/client/slots.ts:253

        if (this._locale === face) this._locale = undefined
      }
    }, 'slots.installLocale()')
  }

  /**
   * The single ctx-level render entry: the shell renders 'root'; every other
   * key renders inside components through the props renderSlot face. All
   * three guards are fail-loud boot-order checks, no fallback.
   * @param key - must be 'root' (runtime-enforced for dynamically composed callers).
   * @param owner - owner share for the root entry (the shell supplies {}).
   * @returns the rendered root tree.
   */
  renderSlot<K extends keyof SlotMap & string>(key: K, owner: OwnerOf<K>): ReturnType<SlotRenderer['renderRoot']> {
    // Widened: in this package's own program SlotMap holds only 'root', which
    // would fold the guard to constant-false; the check exists for plain-JS
    // and cross-program callers where K is wider.
    if ((key as string) !== 'root') {
      throw new Error(`ctx-level renderSlot only renders 'root' (got "${key}"); child slots render through the component props face`)
    }
    if (this._renderer === undefined) {
      throw new Error("slot renderer not installed — boot must call ctx.slots.install(createSlotRenderer()) before rendering 'root'")
    }
    if (this._core.entries('root').length === 0) {
      throw new Error("'root' has no registration — a layout entry must register into 'root' before the shell renders it")
    }
    return this._renderer.renderRoot(this.hostFace(), owner)
  }

  /**
   * Drop the per-session store instances of a dead session (the sessions
   * service calls this on scope teardown; root-scoped records are untouched).
   * Persisted state goes with the session — a never-rendered dead session can
   * still own keys from an earlier page load, so the instance is materialized
   * transiently just to clear storage (no-op for unpersisted stores).
   * @param sessionId - the torn-down session.
   */

View on GitHub (pinned to b150a551b8)

Solutions

  1. Render nested slots from inside the owning component through its props render-slots face, never from ctx.slots
  2. Keep ctx.slots.renderSlot('root', owner) as the single shell-level render call
  3. Map config-supplied slot keys to a registered component's children instead of calling ctx-level renderSlot
  4. Remove `as 'root'`-style casts — they hide the misuse until this runtime throw

Example fix

// before — a cast defeats the type; the runtime guard still throws
ctx.slots.renderSlot(dynamicKey as 'root', owner)

// after — only the shell renders 'root'; nested slots go through the component props face
if (dynamicKey === 'root') {
  ctx.slots.renderSlot('root', owner)
} else {
  // render via the registered component's render-slots share (its children face), not ctx.slots
}
Defensive patterns

Strategy: type-guard

Validate before calling

// guard dynamic keys before the ctx-level render
if (key !== 'root') {
  throw new Error(`cannot ctx-render slot ${key} — route it through the component props face`)
}
ctx.slots.renderSlot(key, owner)

Type guard

function isRootSlot(key: string): key is 'root' {
  return key === 'root'
}

Prevention

When it happens

Trigger: Calling ctx.slots.renderSlot with any key other than 'root' — e.g. a composed slot name like 'tool.call.toolview' — from shell code or from JavaScript; casting a dynamic key with `as 'root'` or a widened `keyof SlotMap & string` so the compiler accepts it (the runtime guard still throws).

Common situations: Trying to render a nested plugin slot directly from app-shell code instead of composing through slots.register; dynamic slot routing driven by config-supplied keys; JS consumers of the client runtime skipping the types entirely.


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