DSH plugin hot reload: cordis.yml, HMR, PENDING diagnosis
A DSH plugin's hot reload mechanism is "unload first, then load": cordis.yml selects the plugin tree to apply, and @deepseek-ai/dsh-hmr watches files and, on save, replaces running plugins by unloading (releasing effects) plus dependency-driven loading. To hot-swap precisely, give each entry an id; when a plugin never loads and reports nothing, it is usually because a service in inject has no provider and the fiber is stuck in PENDING.
How a DSH plugin composes the plugin tree with cordis.yml: id, disabled, and groups
cordis.yml is what decides which plugins are applied—every capability is a plugin, and this file selects the plugin tree (source). Besides name and config, an entry accepts other metadata:
- id: greeter # stable identity for this entry
name: './greeter.ts'
- id: consumer
name: './consumer.ts'
disabled: true # keep the entry, skip mounting it
idprovides a stable identity — it lets the loader tell "modify an existing entry" from "remove then add." Expect: only the affected part is remounted when you change the config.disabled: trueunloads without deleting — the entry is kept and mounting is skipped. Expect: setting it back restores the plugin, and plugins left PENDING because they depend on its service reload too.- A group can nest a sub-list — load and unload a set of entries as one unit. Expect: the whole group goes up and down together.
isolateprovides an independent service instance — isolate a service instance for a group. Expect: two groups each see a provider of the same name with different config, without affecting each other.
Key cause and effect: an entry without an id gets a freshly generated id on every read, so any edit to the config file—even if the entry's own text is unchanged—is treated as remove-then-add and remounted.
How a DSH plugin hot reloads: dsh-hmr unload and reload
HMR works because unloading releases effects and loading follows dependencies, so "unload first, then load" can replace a running plugin (source). Configure and verify in four steps:
- id: logger
name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
name: '@deepseek-ai/dsh-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
- Mount
@deepseek-ai/dsh-hmrincordis.ymland setconfig.rootto the directory to watch (e.g.['.']). Expect: on save the old instance unloads first, then new code loads. - Also mount
@deepseek-ai/cordis-plugin-logger-consoleso HMR records logs through the Cordis logger service. Expect: you see unload / reload messages; without a console exporter you see no output at all. - Also mount
@deepseek-ai/cordis-plugin-timer, thetimerservice HMR injects for debouncing. Expect: the plugin does not stay in PENDING; without timer it stays PENDING forever and emits no hint. - Run it under tsx and save a plugin file as needed, for example
node --import tsx ../../vendor/cordis/bin.js. Expect: every effect of the old instance is rolled back, new code loads andapplyruns again; editingcordis.ymlitself also updates only what changed byid.
To understand effects and unload rollback first, revisit DSH plugin events.
Diagnosing a DSH plugin stuck in PENDING: inject with no provider
If a plugin's inject names a service nobody provides, it waits forever and outputs nothing—this is not an error, because PENDING is a legal state and a provider may mount later (source). Locate it in four steps:
import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
export function apply(ctx: Context) {
setTimeout(() => {
for (const runtime of ctx.registry.values()) {
for (const fiber of runtime.fibers) {
if (fiber.state === FiberState.PENDING) {
console.log(`${fiber.name} is PENDING — a required service is missing`)
}
}
}
}, 500)
}
- Confirm the symptom is "neither acts nor reports" to rule out a thrown error. Expect: no error stack in the terminal and the process still running.
- Enumerate the registry from any context: walk
ctx.registry.values()and eachruntime.fibersinside it. Expect: you get every plugin fiber and its state. - Filter for fibers where
fiber.state === FiberState.PENDING. Expect: the stuck plugin's name prints, showing that a required service in itsinjectis missing. - Add a provider for that service, or drop the
inject. Expect: the fiber moves from PENDING to ACTIVE and starts running.
Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE, because the config file itself is mounted through a plugin.
Three self-checks after writing (good for any DeepSeek Harness plugin): does every commonly used plugin have an id; are HMR's logger and timer dependencies in place; when something "has no effect", did you look at the fiber state first. To apply this diagnosis to a real service, see the Cordis primer; to find ready-made logging or timer plugins, search the DSH Plugin Hub.
FAQ
A DSH plugin uses cordis.yml to select the plugin tree, where id gives an entry a stable identity so the loader can distinguish "modify an existing entry" from "remove then add", and disabled: true unloads the plugin while keeping the entry. Set it back to the original value and the plugin, along with any plugin left PENDING by depending on its service, reloads.
A DSH plugin enables hot reload by mounting @deepseek-ai/dsh-hmr in cordis.yml and setting the watched root directory. HMR records logs through the logger service, so without @deepseek-ai/cordis-plugin-logger-console you never see its messages; it also injects the timer service for debouncing, and without @deepseek-ai/cordis-plugin-timer it stays in PENDING forever without any hint.
Yes, it reloads: a DSH plugin's loader compares entries by id and only mounts, unmounts, or reconfigures the parts that changed. An entry without an id gets a freshly generated id on every read, so any edit to the config file, even when its own text is unchanged, is treated as remove-then-add and remounted.
A DSH plugin keeps waiting and outputs nothing if inject names a service nobody provides, because PENDING is a legal state. In that case check the plugin's fiber state: iterate ctx.registry.values() and runtime.fibers, and a FiberState.PENDING means a required service is missing.
A DSH plugin's groups can nest a sub-list of entries to load and unload several plugins as one unit, while isolate gives a group its own instance of a service name. That way two groups each see a differently configured provider of the same name without affecting each other.
Related Terms
- cordis.yml
- cordis.yml is the plugin-tree configuration source of a DSH plugin, whose `id`, `name`, `config`, and `disabled` fields decide which plugins are applied and in what order, with the loader comparing entries by `id` and hot-updating the difference.— https://deepseek-harness.github.io/deepseek-harness/en/develop/cordis-tutorial/06-composition-and-hmr
- HMR
- HMR is hot module replacement, the mechanism by which a DSH plugin unloads the old instance and loads new code after a file save, triggered by `@deepseek-ai/dsh-hmr` watching files, where unloading rolls back every effect of that plugin.— https://deepseek-harness.github.io/deepseek-harness/en/develop/cordis-tutorial/06-composition-and-hmr
- FiberState.PENDING
- FiberState.PENDING is one of the fiber states of a DSH plugin, meaning the plugin is waiting for a required service, which is a legal state rather than an error and usually appears when a service in `inject` has no provider.— https://deepseek-harness.github.io/deepseek-harness/en/develop/cordis-tutorial/06-composition-and-hmr
- isolate
- isolate is a group-level setting of a DSH plugin that gives one group an independent instance of a service name, so different groups can see same-named providers with different config that do not affect each other.— https://deepseek-harness.github.io/deepseek-harness/en/develop/cordis-tutorial/06-composition-and-hmr