Hot re-apply of an in-place updated plugin package fails with tool "X" already registered; module cache serves stale code #1610
Replies: 2 comments
|
Verified the replace path against master ( The collision requires the registration to survive
|
Uh oh!
There was an error while loading. Please reload this page.
Issue draft — upstream deepseek-harness
Title
Hot re-apply of an in-place updated plugin package fails with
tool "X" is already registered; the loader's module cache also serves the pre-update codeSummary
When a plugin package that is already mounted in the running tree is updated in place on
disk (the plugin market reinstalls/updates it: files under
node_modules/<pkg>are rewritten),re-applying the composition through the root Include fails:
A fresh boot with the same composition works fine (the row is the first registrant). The failure
only happens when the row is re-applied while its previous fiber is still live.
Environment
0.1.0-rc.5dsh-base,dsh-web-app, a tool-registering plugin)Reproduction
ctx.tools.registerplugin).node_modules/<pkg>; the profile manifest / bundle layer is touched).cordis.patch.yml, or thedsh-live-reloadrefresh which calls the sameinclude.update).apply()re-runsctx.tools.register({ name: '...' });the name is still owned by the previous fiber →
already registered→ the whole includeupdate rolls back (transactional) → the refresh reports failure.
Even when the row's options are otherwise identical (so no collision), the loader's ESM module
cache serves the old code for the same package URL — the updated package's new code never
loads without a process restart.
Root-cause analysis
ModuleLoader(
cordis-plugin-loadersrc/internal.ts:ctx.loader.internal.loadCache, keyed by file URL).An in-place package update keeps the same URLs, so
loadCachereturns the pre-updateModuleJob. Module-level HMR is intentionally off on the web surface, but there is nodocumented/supported API to evict a package's cached jobs, so third-party hot-reload tooling
must either restart the process or reach into
loader.internal.loadCache(Node internals).src/config/entry.ts) isimport(candidate.name)→dispose(previous)→start(plugin). If the import hits the stalecache, the "new" apply runs the OLD code while the old fiber may still hold its registrations —
the observed
already registerederror. (The harness's own HMR handles withdrawal fortool-fs'sread_image; the re-apply ordering above is the fragile spot for out-of-treeplugins.)
Expected behavior
(e.g. a
loader.evictPackage(pkgDir)or a cache-busting import path), so in-place packageupdates can be hot-applied without a restart; and
apply()runs, so a re-applied row can never collide with its own previous generation; or,failing that, produce an actionable error ("package X was updated on disk; a restart is
required to load the new code") instead of the cryptic tool-name collision.
Workaround demonstrated today
dsh-live-reload(v0.2.0) detects on-disk package changes (a per-bundle filefingerprint seeded at boot) and re-points the changed package's loader rows at
a cache-busted entry URL (
<entry file URL>?dshr=<rev>), which creates afresh module-cache key per on-disk revision and forces the loader's replace
path (re-import → dispose the old fiber — withdrawing its tool registrations →
start). Verified in its scripted e2e: after an in-place rewrite of a mounted
bundle, a refresh re-imports and runs the new code (marker file v1→v2) with
the same tool name, no collision, no restart. It relies on Node's URL-keyed ESM
cache semantics; a first-class loader API (documented eviction or reload of one
package) would make it robust and remove the need for the query-string trick.
All reactions