DeepSeek Harness plugin HMR reload: cache, timers, effects

TroubleshootingPublished 2026-10-30Author: DeepSeek Plugin Market
DeepSeek HarnessDSHplugin developmentHMRhot reloadINACTIVE_EFFECTalready-installed
HMR reruns apply() yet runs old logic, timers throw nested transaction errors, or the host dies with INACTIVE_EFFECT. Four failures, one triage table.

The trap plugin authors fall into most is not writing bad code but "assuming hot reload took effect": apply() does rerun and the log shows a new activation record, yet the module is still old, the timer still carries last round's transaction marker, or the host crashes outright. These four failures each have their own signals; triage with the table first, then fix them one by one (#10774, #11121).

DeepSeek Harness: Triage table: symptoms and signals for four failures

In one sentence: no reaction to code changes is a watch-scope problem; a reaction with wrong behavior is a cache problem; nested-transaction errors are a marker problem; a host exit is an inactive-context problem.

TypeSymptomKey signalBelongs to
1. Module not refreshedapply() reruns, logic unchangedactivation log but no new behaviorwatch scope / ESM cache
2. Poisoned timerone nested-transaction error per cyclecold start fine, always fails after hot reloaddsh-hmr defect
3. Host crashapp exits outrightINACTIVE_EFFECTplugin apply code
4. Update refusedupdating reports already-installedappears only on rc.3+version behavior

DSH plugin: Type 1: config hot reload is not module hot reload

hmr.config.root defaults to an empty array and only watches config files; a file:// plugin on another drive is not in the watched scope at all.

  1. Confirm the symptom: every touch of cordis.patch.yml adds another apply() record, which means the plugin was rebuilt.
  2. But the imported module is not re-evaluated — Node's ESM cache is keyed by resolved URL, and an unchanged URL keeps the old module.
  3. Check the watch scope: the base bundle sets hmr.config.root to [], so source watching is off; profile recomposition only covers the profile directory and linked package targets.
  4. Fix it by configuring both base and root in the hmr entry:
yaml
- id: hmr
  disabled: false
  config:
    base: 'file:///F:/path/to/your/repo/'
    root: ['plugin/dsh-live-workbench', 'server']
  1. base only anchors relative paths; root decides what is actually watched, so changing only base is the same as not enabling watching.
  2. Make the plugin entry a static import of its handler so HMR has a dependency path to replace; a dynamic import(...?v=timestamp) only refreshes the entry.

The community ran a two-stage check on the packaged desktop app: with root: ['.'], both entry changes and static dependency changes reloaded; narrowed to two directories, both still reloaded. The ?v= route has a ceiling — it re-evaluates only the entry, and sibling modules statically imported by the entry stay old.

DeepSeek Harness: Type 2: timers permanently carry the transaction marker

apply() executes inside an HMR transaction, and timers registered there capture the marker; the transaction ends milliseconds later, but the timer keeps it.

  1. Reproduce: the plugin uses ctx.interval() in apply() for a 15-second heartbeat whose callback does a config write such as saveSelection().
  2. Cold start is fine and hmr.executing.getStore() is undefined.
  3. After hot reload, getStore() is true inside every callback.
  4. So runExclusive() rejects right at the entry: HMR transactions cannot be nested.
  5. The symptom is that every capability requiring a config write silently stops working, printing one error per cycle, until restart.
  6. The community's minimal reproduction (plain Node + AsyncLocalStorage) had 0 failures on cold start and 3/3 failures after hot reload.

The fix is not "clear the marker before entering the transaction". The community measured that this lets genuine nested calls through too: an inner task queues behind an outer one that is waiting on it, neither Promise can settle, and every later runExclusive on that instance never returns. The workable version gives each transaction an independent state object, sets active to false when it ends, and updates the other three read sites in the same file to getStore()?.active.

DSH Plugin: Type 3: the host dies with INACTIVE_EFFECT

The plugin's apply is async and never rechecks whether the fiber is still alive after the first await; when hot reload destroys the context before the second child plugin mounts, it crashes.

  1. Read the crash log: CordisError: cannot create effect on inactive context, with code: 'INACTIVE_EFFECT'.
  2. Read the stack: Fiber._reload → plugin() → assertActive.
  3. Read the code shape: apply does await ctx.plugin(workspaceDependencies, ...), then synchronous path work, then a second await ctx.plugin(officeSkills, ...).
  4. Trigger: hot reload destroys or replaces that fiber while the first await is suspended, so the context is already inactive at the second mount.
  5. Fix one: if the two child plugins do not depend on each other, mount both in one synchronous turn.
  6. Fix two: recheck whether the fiber is still active after every await, and return early when it is not.

This error is not a Cordis defect; assertActive behaves correctly. The caller crossed the invalidation point.

DeepSeek Harness: Type 4: the official manager refuses overwrite installs

In HMR mode, as soon as the profile dependency already contains a package with that name, the official manager refuses outright — pnpm never even runs.

  1. Read the error: official plugin manager update failed: refused to execute (already-installed).
  2. Match the version: rc.1 and rc.2 support overwrite updates; rc.3, rc.4, and rc.5 do not, as the community measured version by version.
  3. Recovery path: uninstall the plugin → official restart → reinstall.
  4. That is currently the only stable path, and the upgrade guide already documents removeBundle followed by installBundle.
  5. To bypass it you can use DSH's bundled pnpm with add <package>@<version> --save-exact, but that is manual intervention, not a UI capability.
  6. The root fix is an atomic updateBundle: install into a temp directory and swap, leaving the profile untouched on failure.

DSH plugin: Cautions

Prove a reload with behavior, not with log lines.

  1. Do not use "an activation log appeared" as proof hot reload worked. It proves only that apply() reran.
  2. Do not configure only base. With an empty root, source watching is still off.
  3. Do not register timers that write config inside apply(). They always break after hot reload unless core fixes it.
  4. Do not use ctx directly after an await. Recheck liveness after every asynchronous step.
  5. Cold-start once after changing a plugin. Defects in the hot-reload path easily mask real logic problems.

If none of the four match, first confirm whether your plugin is registered through a profile patch as a file:// path — that is the most common starting point.See DeepSeek Harness plugin load: already registered, pending for the related case.

FAQ

I changed plugin code and touched cordis.patch.yml, but DeepSeek Harness still runs the old logic — why?

Config hot reload and module hot reload are two separate paths in DeepSeek Harness. The base bundle sets hmr.config.root to [], so it watches only config files and not source; a file:// plugin on another drive is outside the watched root anyway. Configure both base and root in the hmr entry.

After hot reload in DeepSeek Harness, a plugin timer keeps reporting HMR transactions cannot be nested until restart — why?

The timer keeps failing because DeepSeek Harness runs apply() inside an HMR transaction, and timers registered there get the transaction marker pinned by AsyncLocalStorage; the transaction ends but the marker stays. Any runExclusive() in the callback is rejected by its own nesting check until the process restarts.

Is clearing the outer marker when fixing runExclusive in DeepSeek Harness a good idea?

No, clearing the outer marker is not a good fix in DeepSeek Harness, because the community measured that it lets genuine nested calls through too: an inner task queues behind an outer one that is waiting on it, the queue deadlocks permanently. A workable version gives each transaction its own state object with active, set to false when it ends.

The DeepSeek Harness desktop host dies with INACTIVE_EFFECT during plugin hot reload — is that a Cordis bug?

No, it is not a Cordis bug: in DeepSeek Harness apply is an async function and does not recheck whether the fiber is still alive after the first await ctx.plugin(...), so when hot reload destroys the context and a second ctx.plugin runs, it throws. Fix it by mounting both child plugins synchronously, or rechecking after each await.

The DeepSeek Harness plugin manager reports already-installed when updating a plugin — what should I do?

In HMR mode the official DeepSeek Harness plugin manager does not allow overwriting an installed plugin. The recovery path is uninstall → official restart → reinstall. The community measured that rc.1 and rc.2 allow overwrite updates, while rc.3 through rc.5 always show this error.

Related Terms

HMR
HMR is the DeepSeek Harness hot module replacement mechanism that rebuilds plugins when config files or source change. Config hot reload and module hot reload are independent paths, and only the former is on by default.— https://github.com/deepseek-ai/deepseek-harness/discussions/10774
runExclusive
runExclusive is the transactional method in dsh-hmr that serializes operations, marking a transaction in progress with AsyncLocalStorage. It checks for an existing marker on entry and rejects a hit as a nested transaction.— https://github.com/deepseek-ai/deepseek-harness/discussions/11121
INACTIVE_EFFECT
INACTIVE_EFFECT is the error Cordis throws when creating an effect on an already disposed context. It usually means an asynchronous operation crossed the point where the fiber was destroyed.— https://github.com/deepseek-ai/deepseek-harness/discussions/11306
already-installed
already-installed is the error the official plugin manager returns when it refuses to overwrite an installed plugin in HMR mode. It reflects a version behavior difference rather than plugin corruption.— https://github.com/deepseek-ai/deepseek-harness/discussions/11039

Sources