Repository navigation
Desktop 0.2.1-rc.2:更新插件市场会让应用退出、profile 留在“bundles 条目丢失但依赖还在”的半卸载状态 #10039
Replies: 3 comments
|
补充一条同一次事故里的宿主侧新现场(比自更新本身更严重:它让所有插件操作都失败)。 现象2026-10-18 01:53(UTC+8)崩溃重启后,
为什么不会自愈
处理按 atomic-write 注释里的口径(“a live process reused keeps its lock until an operator removes it”),
建议
这条与自更新的关系:自更新/崩溃是触发场景,但“残留锁 + 只认已退出 PID”是宿主侧独立缺陷, |
|
Your log is complete enough to place the crash between two specific lines, and both of your findings are real and independent. Source references below are 1. Why the host did not refuse — and which guard was skipped
if (this.ownerContext.get('hmr') === undefined && (this.profile.startedBundles.includes(name) || ...)) {
throw new ManagementFailure('stop-profile') // :691, :694
}
const contributions = bundle.error === undefined ? this.bundleRows(name) : []
if (bundle.enabled) {
await this.selectBundle(name, false) // :698 — persists the manifest without the entry
Object.assign(result, await this.reload()) // :699 — unloads the bundle's rows
}
if ([...this.ctx.loader.entries()].some(entry => entry.fiber?.uid != null && ...)) {
throw new ManagementFailure('bundle-in-use') // :703
}Your profile runs 2. Why the half-state looks exactly like that
3. Startup reconciliation would not heal it as-is — and the existing function cannot either
It is also asymmetric in a way that matters for the fix: it only ever adds a bundle for a dependency that is new to that run — for (const name of dependencies) {
if (beforeDeps.has(name)) continue // :99
const metadata = bundleManifest(name, dir, anchor)
if (metadata?.dsh?.bundle === undefined) { ...continue } // :101
...
}
4. On your item 1 — the host does protect itself, but only against the wrong thingThere is a self-protection guard, and it is the right idea keyed to the wrong predicate. Inside if (!enabled && previous.includes(name)) {
if (this.protectsManager(name)) throw new ManagementFailure('management-required') // :820-821
}
That is the exact class distinction your report is asking for: "cannot remove the component that manages plugins" already exists, and needs one step further — a bundle that is currently serving the request must not be removable either. Two shapes fit: key the refusal on the caller (the request's owning bundle), or let a bundle declare in its own manifest that it cannot be removed live, which the host then honours the way it honours 5. On your item 5 — the half-state is already reportable, without host changes
6. The orphaned lock is the sharper of your two findingsYour reading of Two remedies that fit the existing design: allow takeover past a staleness bound even for a live pid (a manifest write is seconds, not minutes), and give the failure a named exit instead of a bare Agreed with your priority: the lock is the one to fix first — it converts one bad update into a profile that cannot be repaired from the UI at all. |
宿主侧新现场:repair 之后磁盘已修复,但运行中的 host 不重挂载 → 整组工具静默消失承接上面关于 时间线(UTC+8,profile
|
| 时间 | 事件 |
|---|---|
| 10-18 01:42:51 / 01:51:02 | pnpm 操作把 dsh-free-search 0.9.1 从依赖中摘除(.plugin-manager/logs/operation-*/pnpm.log 里的 - dsh-free-search 0.9.1) |
| 01:53:27 | 宿主重启(PID 60428),此时运行组合里没有该插件 |
| 01:56:27 | 应用自动 repair,生成 package.json.repair-97-20261018-015627;与当前文件 diff 显示 repair 把 dsh-free-search 写回 dependencies 和 dsh.profile.bundles |
| 01:59 起 | 会话里 9 个工具整组消失:advanced_search / cache_search / domain_search / free_search_test / multi_search / platform_search / research / video_search / weather;请求头只剩宿主自带的 web_search / web_fetch |
现场特征
- 是一条
tool-removal同时删掉 9 个工具(source =tool-registry),不是逐个失败; - UI 没有任何提示,模型侧只得到
Error: unknown tool "domain_search",看起来像单个工具坏了; - 磁盘侧全对:
package.json里依赖与 bundles 条目都在、node_modules/dsh-free-search完整、node -e "import('dsh-free-search')"正常导出。
也就是说,manifest 与 live tree 之间的漂移是完全不可观测的:用户、UI、模型三方都拿不到"这个插件没有被挂载"这一事实,只能从 unknown tool 反推。@argszero/dsh-activation-doctor 能报 "installed but never selected",但它比对的是 manifest ↔ dependencies;本例里 manifest 已经被 repair 修好,漂移发生在 manifest ↔ 运行中的挂载集合,这一层目前没有工具能报。
两点建议
- 任何写
dsh.profile.bundles/dependencies的路径(包括 repair)在宿主存活时,要么触发一次reload(),要么显式置 "restart required" 状态并在 UI 常驻——否则会出现"磁盘已修好、功能仍缺失"的永久落差,用户无从判断该不该重启; tool-removal目前只是 developer 消息里的一条静默事件。当移除原因是"整个插件离线"时,建议升级为一条会话可见的提示(例如在系统提示里写明"以下插件已离线:dsh-free-search"),否则 agent 只能靠猜测某个工具为何报 unknown tool。
(同一事故的市场侧线索已追加在 dsh-market/dsh-market#900:already-installed 更新失败 + 回滚不可验证 → 插件被摘除 → 10-18 当天市场日志 0 条。)
Uh oh!
There was an error while loading. Please reload this page.
Desktop 0.2.1-rc.2:更新插件市场会让应用退出、profile 留在“bundles 条目丢失但依赖还在”的半卸载状态
环境
DeepSeek Harness.exeFileVersion 0.2.1-rc.2)desktop,patchReload: live(HMR 开启)dshmarket@1.66.27(后升级到 1.66.35)、dsh-free-search@0.9.0现象(可复现两次)
在市场里点“更新市场”(更新 dshmarket 自己):
pluginManager.installBundle("dshmarket@x");{"code":"already-installed"}拒绝(HMR 下对已安装包直接 add 会被拒,插件管理器README 也写明“装另一个版本需要先卸载再安装”);
pluginManager.removeBundle("dshmarket");package.json的dsh.profile.bundles里dshmarket条目消失,但dependencies.dshmarket与node_modules/dshmarket都完好(“假卸载”);package.json。时间点(UTC+8):
already-installed,rollback 同错;dshmarket的package.json;.dsh-market/log.ndjson与.plugin-manager/logs都没有这次操作的任何记录(没有新的
operation-*,pnpm 没有跑)。期望
removeBundle不应允许移除“当前正在处理该请求的 bundle”,或至少要求显式确认;updateBundle/restartToApply之类的接口,调用方不需要remove+re-add(现在的组合在“更新自己”时必然出问题);
removeBundle真正动手前先落一条日志,崩溃时至少能查到“谁要卸谁”;dsh.profile.bundles,自动恢复“假卸载”半状态;证据
~/.dsh/profiles/desktop/package.json:dependencies.dshmarket存在,dsh.profile.bundles缺条目.dsh-market/log.ndjson:22:54 的already-installed与update-rollback记录.plugin-manager/logs/operation-KOq7p0/pnpm.log:更早的一次 remove;本次操作没有对应目录[Bug][Desktop] 自更新会把 dshmarket 自己卸载掉:removeBundle 后 installBundle 未执行,市场从 profile 消失(1.66.33 / 1.66.34 实证) dsh-market/dsh-market#900
备注
插件市场 1.66.35 的
installOrReinstall()仍是removeBundle+ 重新安装,没有对“目标是自己”的保护;我核对过 npm tarball 与本机安装目录。因此这个问题的修复点更适合放在宿主侧(见“期望”1/3/4)。
All reactions