DeepSeek Harness 插件热重载失败:模块缓存、定时器与 INACTIVE_EFFECT
插件作者最容易踩的坑不是写错代码,而是「以为热重载生效了」:apply() 确实重跑了一遍,日志里也出现了新的激活记录,但模块还是旧的、定时器还带着上一轮的事务标记、甚至 host 直接崩掉。 这四类失败各有各的判据,先按自查表分诊,再逐条修(#10774、#11121)。
DeepSeek Harness 自查表:四类失败的症状与判据
一句话分诊:改代码没反应是监听范围问题;改代码有反应但行为不对是缓存问题;定时任务报嵌套是事务标记问题;host 直接退出是上下文失活问题。
| 类型 | 现象 | 关键判据 | 归属 |
|---|---|---|---|
| 一、模块不刷新 | apply() 重跑,逻辑仍旧 | 有激活日志、无新行为 | 监听范围/ESM 缓存 |
| 二、定时器带毒 | 每周期一条嵌套事务错误 | 冷启动正常、热重载后必错 | dsh-hmr 缺陷 |
| 三、host 崩溃 | 应用直接退出 | INACTIVE_EFFECT | 插件 apply 写法 |
| 四、更新被拒 | 更新插件报 already-installed | 只在 rc.3+ 出现 | 版本行为 |
DSH插件 第一类:配置热更不等于模块热更
hmr.config.root 默认是空数组,只监听配置文件;file:// 插件放在别的盘上,压根不在监听范围内。
- 确认现象:每次 touch
cordis.patch.yml都会多一行apply()记录,说明插件重建了。 - 但导入的模块没重新求值——Node 的 ESM 缓存按解析后的 URL 计,URL 不变就用旧模块。
- 查监听范围:base bundle 把
hmr.config.root设为[],源码监听是关的;profile 重组只覆盖 profile 目录与链接包目标。 - 修法:在
hmr条目里同时配base与root:
- id: hmr
disabled: false
config:
base: 'file:///F:/path/to/your/repo/'
root: ['plugin/dsh-live-workbench', 'server']
base只是给相对路径提供锚点,真正决定监听什么的是root;只改base等于没开监听。- 把插件入口改成对 handler 的静态导入,HMR 才有依赖路径可以替换;动态
import(...?v=时间戳)只是入口刷新。
社区在打包版桌面端做过两阶段验证:root: ['.'] 时改入口和改静态依赖都能重载;收窄到两个目录后同样都重载。但 ?v= 这条路有天花板——它只重新求值入口,入口静态导入的兄弟模块仍然是旧的。
DeepSeek Harness 第二类:定时器永久带着事务标记
apply() 在 HMR 事务里执行,里面注册的定时器把事务标记捕获下来了;事务几毫秒后结束,定时器却还带着它。
- 复现:插件在
apply()里用ctx.interval()起 15 秒心跳,回调里做saveSelection()之类的配置写入。 - 冷启动正常,
hmr.executing.getStore()是 undefined。 - 热重载后,每次回调里
getStore()都是 true。 - 于是
runExclusive()在入口处直接拒绝:HMR transactions cannot be nested。 - 表现是插件所有需要写配置的能力静默失效,每周期打印一条错误,直到重启。
- 社区的最小复现脚本(纯 Node + AsyncLocalStorage)在冷启动下 0 次失败,热重载后 3/3 失败。
修法不是「进事务前清掉标记」。社区实测那样会连真正的嵌套调用也放行:内层任务排在外层后面、外层又等内层,两个 Promise 都结束不了,之后该实例上所有 runExclusive 都不再返回。可行版本是给每次事务一个独立状态对象,结束时把 active 置 false,并在同文件另外三处读取点一并改成 getStore()?.active。
DeepSeek插件 第三类:host 崩在 INACTIVE_EFFECT
插件的 apply 是 async 函数,第一次 await 之后没有再检查 fiber 是否还活着,热重载把上下文销毁后再挂第二个子插件就会崩。
- 看崩溃日志:
CordisError: cannot create effect on inactive context,code: 'INACTIVE_EFFECT'。 - 看栈:
Fiber._reload→plugin()→assertActive。 - 看代码形状:
apply里第一个await ctx.plugin(workspaceDependencies, ...),中间做同步路径处理,第二个await ctx.plugin(officeSkills, ...)。 - 触发条件:热重载在第一次 await 挂起期间销毁或替换了该 fiber,第二次挂载时上下文已失效。
- 修法一:两个子插件互不依赖,直接在一个同步 turn 里挂完。
- 修法二:每次
await之后复检 fiber 是否仍活跃,不活跃就直接返回。
这个错误不是 Cordis 的缺陷,assertActive 的行为是对的;问题在调用方跨越了失效时间点。
DeepSeek Harness 第四类:官方管理器拒绝覆盖安装
HMR 模式下,只要 profile 依赖里已有同名包,官方管理器就直接拒绝,连 pnpm 都不会跑。
- 看报错:
官方插件管理器更新失败:官方插件管理器拒绝执行(already-installed)。 - 按版本对号:rc.1、rc.2 支持覆盖更新;rc.3、rc.4、rc.5 不再支持,社区逐版本实测。
- 恢复路径:卸载插件 → 官方重启 → 重新安装。
- 这是目前唯一稳定路径,升级指南里本来就有 removeBundle 再 installBundle 的流程。
- 想绕过也可以用 DSH 自带的 pnpm 执行
add <包>@<版本> --save-exact,但那是手工介入,不属于界面能力。 - 根本修法是提供原子的
updateBundle:临时目录装好再替换,失败时 profile 不变。
DSH插件 注意事项
下面这些做法会加重问题或掩盖真因,先避开再谈修复。
- 别用「激活日志出现」证明热重载成功。它只证明
apply()重跑了。 - 别只配
base。root为空时源码监听仍然是关的。 - 别在
apply()里注册带配置写入的定时器。热重载后必炸,除非内核修好。 - 别在 await 之后直接用 ctx。每一步异步之后都该复检活跃状态。
- 改完插件先冷启动验证一次。热重载路径的缺陷容易掩盖真实逻辑问题。
如果四类都对不上,先确认你的插件是不是通过 profile patch 注册的 file:// 路径——那是最常见的起点。相关排查见 DeepSeek Harness 插件装上了不生效:重名注册、realm pending 与重载报错。
常见问题
因为 DeepSeek Harness 的配置热更和模块热更是两条路。base bundle 把 hmr.config.root 设为 [],默认只监听配置文件不监听源码;file:// 插件不在 profile 根内也不会被监听。要在 hmr 条目里同时配 base 和 root。
因为 DeepSeek Harness 里 apply() 是在 HMR 事务里被调用的,里面注册的定时器被 AsyncLocalStorage 钉住了事务标记,事务结束后标记还在。回调里任何 runExclusive() 都会被自己的嵌套检查拒绝,直到进程重启。
不行,DeepSeek Harness 里清掉外层标记会让真正的嵌套调用也被放行。社区实测那样会连真正的嵌套调用也放行,内层任务排在外层后面、外层又等内层,队列永久卡死。可行做法是给每次事务一个带 active 的状态对象,结束后置 false。
不是,这是 DeepSeek Harness 的 async apply 缺少活跃复检造成的。apply 是 async 函数,第一次 await ctx.plugin(...) 之后没有再检查 fiber 是否还活跃,热重载把上下文销毁后再执行第二个 ctx.plugin 就会抛错。修法是一次同步挂载两个子插件,或 await 后复检。
HMR 模式下 DeepSeek Harness 的官方管理器不允许覆盖已装插件。恢复路径是「卸载 → 官方重启 → 重新安装」。社区实测 rc.1、rc.2 可以覆盖更新,rc.3 到 rc.5 必现这个错误。
相关术语
- HMR
- HMR 是 DeepSeek Harness 的热模块替换机制,负责在配置文件或源码变化时重建插件。配置热更与模块热更是两条独立路径,默认只开前者。— https://github.com/deepseek-ai/deepseek-harness/discussions/10774
- runExclusive
- runExclusive 是 dsh-hmr 里把操作串行化的事务方法,用 AsyncLocalStorage 标记事务进行中。它在进入前检查已有标记,命中就拒绝为嵌套事务。— https://github.com/deepseek-ai/deepseek-harness/discussions/11121
- INACTIVE_EFFECT
- INACTIVE_EFFECT 是 Cordis 在已失效的上下文上创建 effect 时抛出的错误码。它通常意味着异步操作跨越了 fiber 被销毁的时间点。— https://github.com/deepseek-ai/deepseek-harness/discussions/11306
- already-installed
- already-installed 是官方插件管理器在 HMR 模式下拒绝覆盖已安装插件时返回的错误。它属于版本行为差异,不是插件损坏。— https://github.com/deepseek-ai/deepseek-harness/discussions/11039
来源
- #10774 — Plugin patch hot-reload re-runs apply() but keeps already-imported ESM modules cached· deepseek-ai(GitHub Discussions)
- #11121 — 热重载后 apply() 定时器永久携带 executing=true· deepseek-ai(GitHub Discussions)
- #11306 — Desktop host dies with CordisError INACTIVE_EFFECT during plugin hot-reload· deepseek-ai(GitHub Discussions)
- #11039 — 插件管理器在 HMR 模式下拒绝原地升级已经装好的插件· deepseek-ai(GitHub Discussions)