DSH plugin hot reload: cordis.yml, HMR, PENDING diagnosis

Plugin DevelopmentPublished 2026-10-02Author: DeepSeek Plugin Market
DSH pluginDeepSeek Harnesshot reloadHMRcordis.yml
DeepSeek Harness DSH plugin hot reload: cordis.yml composes the tree, dsh-hmr unloads/reloads on save, ids enable hot-swap, PENDING means a missing provider.

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:

yaml
- id: greeter          # stable identity for this entry
  name: './greeter.ts'
- id: consumer
  name: './consumer.ts'
  disabled: true       # keep the entry, skip mounting it
  1. id provides 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.
  2. disabled: true unloads 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.
  3. 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.
  4. isolate provides 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:

yaml
- 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'
  1. Mount @deepseek-ai/dsh-hmr in cordis.yml and set config.root to the directory to watch (e.g. ['.']). Expect: on save the old instance unloads first, then new code loads.
  2. Also mount @deepseek-ai/cordis-plugin-logger-console so HMR records logs through the Cordis logger service. Expect: you see unload / reload messages; without a console exporter you see no output at all.
  3. Also mount @deepseek-ai/cordis-plugin-timer, the timer service HMR injects for debouncing. Expect: the plugin does not stay in PENDING; without timer it stays PENDING forever and emits no hint.
  4. 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 and apply runs again; editing cordis.yml itself also updates only what changed by id.

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:

ts
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)
}
  1. 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.
  2. Enumerate the registry from any context: walk ctx.registry.values() and each runtime.fibers inside it. Expect: you get every plugin fiber and its state.
  3. Filter for fibers where fiber.state === FiberState.PENDING. Expect: the stuck plugin's name prints, showing that a required service in its inject is missing.
  4. 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

What do id and disabled do in a DSH plugin cordis.yml?

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.

How does a DSH plugin enable hot reload, and why no effect?

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.

Does editing cordis.yml itself reload a DSH plugin?

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.

Why does a DSH plugin never load and never report an error?

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.

What are groups and isolate for in a DSH plugin?

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

Sources