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
- Render nested slots from inside the owning component through its props render-slots face, never from ctx.slots
- Keep ctx.slots.renderSlot('root', owner) as the single shell-level render call
- Map config-supplied slot keys to a registered component's children instead of calling ctx-level renderSlot
- 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
- Reserve ctx.slots.renderSlot for the shell's single 'root' render
- Declare nested slots in register({ children }) and render them through the component's props face
- Never cast slot keys to satisfy the type — the runtime guard is the designed backstop
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.