deepseek-ai/deepseek-harness · error
client-modules: ctx.baseUrl is unset — the node half needs t
Error message
client-modules: ctx.baseUrl is unset — the node half needs the config-tree anchor to resolve plugin packages
What it means
ClientModuleRegistry's node half resolves every composed plugin package through createRequire(ctx.baseUrl) — baseUrl is the cordis.yml config-tree directory, whose package declares every composed plugin as a dependency, the only anchor that survives pnpm's isolated node_modules. If the Context has no baseUrl, the service refuses to construct. This is a fail-loud configuration error: the plugin was mounted outside a Loader config tree.
Source
Thrown at packages/client/modules/src/index.ts:308
private readonly rebuildListeners = new Set<(id: string, rev: string) => void>()
private readonly graphListeners = new Set<() => void>()
private readonly dirty = new Set<string>()
private readonly resolvePkgJson: (spec: string) => string
private flushQueued = false
private composed: WebBootGraph
/**
* Build the service: subscribe, seed, and run the activation flush.
* @param ctx - plugin context carrying webServer and loader.
*/
constructor(ctx: Context) {
super(ctx, 'clientModules')
// Resolution anchor: the config tree's baseUrl (the cordis.yml directory,
// whose package declares every composed plugin as a dependency). The
// modules package's own URL would miss sibling packages under pnpm's
// isolated node_modules.
if (ctx.baseUrl === undefined) {
throw new Error('client-modules: ctx.baseUrl is unset — the node half needs the config-tree anchor to resolve plugin packages')
}
const require = createRequire(ctx.baseUrl)
this.resolvePkgJson = spec => require.resolve(`${spec}/package.json`)
// Subscribe before seeding so a fiber arriving mid-activation lands in the
// same dirty set (Set idempotence makes the overlap harmless). An entry-less
// fiber is a child plugin or a manual mount — never a loader row; O(1) drop.
ctx.on('internal/plugin', (fiber) => {
const entryName = fiber.entry?.options.name
if (entryName === undefined) return
this.dirty.add(entryName)
if (this.flushQueued) return
this.flushQueued = true
queueMicrotask(() => {
this.flushQueued = false
this.flush((err) => { ctx.logger.warn(err) })
})
})View on GitHub (pinned to b150a551b8)
Solutions
- Boot through the Cordis Loader with a real cordis.yml so the config tree sets ctx.baseUrl to the config directory.
- If embedding programmatically, anchor the context at the directory whose package.json declares the composed plugins as dependencies before applying client-modules.
- In tests, use REAL composition: boot a test cordis.yml through the Loader instead of ctx.plugin(...) on a bare context.
Example fix
// before — programmatic mount on a bare context
const ctx = app.root.ctx // ctx.baseUrl === undefined
ctx.plugin(ClientModuleRegistry) // throws
// after — load a config tree so the registry anchors at the cordis.yml directory
const loader = new Loader({ root: appDir })
const ctx = await loader.load('cordis.yml') // ctx.baseUrl = dirname(cordis.yml) Defensive patterns
Strategy: validation
Validate before calling
import type { Context } from '@deepseek-ai/cordis'
// guard before mounting client-modules
if (ctx.baseUrl === undefined) {
throw new Error('client-modules needs a Loader config tree — boot from a cordis.yml')
}
ctx.plugin(ClientModuleRegistry) Type guard
import type { Context } from '@deepseek-ai/cordis'
const hasConfigTreeAnchor = (ctx: Context): ctx is Context & { baseUrl: string } =>
typeof ctx.baseUrl === 'string' Prevention
- Boot web profiles through the Loader with a real cordis.yml; never hand-build Contexts for package-resolving plugin sets.
- Anchor resolution at the config directory whose package declares the plugin dependencies — the modules package URL misses pnpm siblings.
- In tests, boot a test cordis.yml (REAL composition) instead of ctx.plugin on a bare context.
When it happens
Trigger: Programmatically mounting ClientModuleRegistry (directly or via a plugin composition) onto a hand-built Context that never loaded a cordis.yml — e.g. ctx.plugin(ClientModuleRegistry) in a test harness or an embedded boot that bypasses the Loader's config-tree load, leaving ctx.baseUrl undefined.
Common situations: Unit tests constructing bare Contexts and applying plugins directly; embedding the web app in another Cordis application that boots plugins from code instead of a config file; a custom entry point that mounts client-modules before any config tree exists.
Related errors
- context-unavailable
- ${binName}: profile bundle ${JSON.stringify(packageName)} de
- client-modules: window.__DSH_BOOT__ is missing or not an obj
- client-modules: boot manifest rev must be a string
- web boot: window.__ModuleLoader__ bootstrap facade is missin
AI-assisted analysis of deepseek-ai/deepseek-harness@b150a551b8 (2026-08-24).
Data as JSON: /api/errors/4baaaa6d80edb3c0.
Report an issue: GitHub.