Repository navigation
dsh CLI on Windows: entry import failures — invisible on 0.1.5-rc.2, downgraded to a warning but still opaque on 0.2.0-rc.2 #9553
Replies: 2 comments
Where the reason goes, and a seam a plugin can use todayYour second defect reproduces as described, and on the loader side it is one line. I traced it on Why
|
|
@argszero — this is the answer we were hoping for, and the Confirming your trace independently We reproduced the two structural facts on 0.2.1-rc.2 as well:
Your framing — "the exception died inside the loader" — is precisely what we observed, and we had been reading the literal as "the error was never available" rather than "it is available one layer down". What we'll try with the seam The part that matters for us is that A related shape in the same class Separately, we had a case where a preset's skill list came back completely empty and the only user-visible symptom was the word Thanks again; the probe is a clean piece of work. |
Uh oh!
There was an error while loading. Please reload this page.
中文摘要(先看这一段即可,英文正文在其后)
现象(
0.1.5-rc.2实测)在 Windows 上,用 npm 装的 dsh CLI 启动时会无声失败:它先正常打印出一个带临时口令(token)的网址,然后进程直接退出、退出码 1。屏幕上唯一的提示是:
而那句"see the error(s) logged above(见上文记录的错误)"所指的上文——从未出现过。任何 profile 都一样:新造的 profile 和机器上原有的
webprofile 都起不来;官方桌面版不受影响。现象(
0.2.0-rc.2实测,同一支探针)同一支"导入期必抛错"的插件,在
0.2.0-rc.2上不再拖垮启动:进程继续运行并正常提供网页服务(有网址、HTTP 303、观察窗内未退出),只在 stderr 留两行:但抛出的真实错误原文,两个版本都看不到。
真正的病因(一句话)
一个名为
@deepseek-ai/dsh-win32-process的零件包,在本机那棵既有的0.1.5-rc.2安装树里被放下了四份一模一样的副本(实测)。其中两份会在同一个进程里先后被加载,而每一份在被加载的那一刻都会去 koffi(一个调用 Windows 系统接口的底层库)的全机唯一登记簿上登记一个名字DSH_STARTUPINFOW:(限定:这是"某棵树的既有状态",不是该版本的必然产物——把全新的
0.1.5-rc.2装进空目录时,本机实测只有 1 份副本。)第二次登记被驳回时抛出的错误正是:
Error: Duplicate type name 'DSH_STARTUPINFOW'。抛错位置:…\dsh-sandbox-local\node_modules\@deepseek-ai\dsh-sandbox-windows-acl\node_modules\@deepseek-ai\dsh-win32-process\lib\index.js:35。为什么使用者看不到原因(第二个独立缺陷,两个版本各有其形)
Cordis 的插件加载器在
Entry._init()里这样写(cordis-plugin-loader\lib\index.js:448-462):在
0.1.5-rc.2(实测):随后dsh-app-boot的assertEntriesLoaded(该版:1434-1440)只检查"有没有座位",把这条报成"无法解析"(could not be resolved)——措辞本身是误导,因为解析其实没问题(下面有反证);而本该解释清楚的另一段检查assertEntriesActivated(该版:1465-1488,它会打印"在等哪个服务")永远轮不到执行,因为它被前面那句抢先抛错中断了。再加上--dump-config(只做静态检查、不真正启动)能通过,所以静态闸也拦不住。在
0.2.0-rc.2(实测 + 读码):这套检查已被改写。assertEntriesLoaded/assertEntriesActivated连同could not be resolved、see the error(s) logged above这两句措辞,在该版dsh-app-boot里已不存在;取而代之的auditStartupEntries(:4009-4020)按一张必需名单requiredStartupEntryIds(:3836-3844,仅含agent-loop、webserver、modules、connection、headless-runner、acp、sdk-jsonrpc-server)判定致命性:名单内条目失败才throw,其余只warn,并明确"让成功的兄弟条目继续运行"。提示语也改为点名式:N entry did not activate+<id> (<name>): failed to import。代价是:
inactiveEntries对"没有座位"这一情形写的是硬编码字符串"failed to import"(该版:3905-3913),不携带被Entry._init()吞掉的那条异常——所以错误原文依旧不可见(实测:注入的标记在 stdout / stderr 均未出现)。关键反证(说明"解析"没问题)
import这个包createRequire(profileDir).resolve(...)--dump-config也就是说:单独看它没问题,放进完整启动树里才炸。
怎么把被吞掉的错误挖出来(手法可复用)
用
--patch临时挂一个自己写的小诊断插件(不落盘、不改 profile),在它的apply()里做两件事:ctx.logger.error包一层,让它同时写到 stderr;ctx.loader.import(name, () => [])——这个入口会把错误抛给调用者,不吞。一试即得
Duplicate type name 'DSH_STARTUPINFOW'。(注意:
--patch是 dsh 的主命令选项,必须排在透传给 web app 的--port/--no-open之前,否则会报unknown option。)问题现状与建议(据
0.2.0-rc.2实测更新)0.2.0-rc.2实测):已做到条目级可见(点名哪一条失败)且不再拖垮启动(降级为 warning,成功的兄弟条目继续服务)。仍未解决:失败原因只是硬编码的failed to import,不携带真实异常——使用者知道"某条没起来",仍不知道"为什么"。建议把Entry._init()捕获到的那条错误一并带出(重抛,或至少在 warning 里附上原文)。@deepseek-ai/dsh-win32-process只有一份物理副本(提升到顶层,或让两个依赖方共用一份)0.1.5-rc.2与0.2.0-rc.2各装进空目录,两者都只有 1 份副本;出问题的是一棵既有的0.1.5-rc.2树(4 份,疑由多轮安装累积)。因此建议:把"单个安装树内dsh-win32-process副本数必须为 1"作为安装/升级后的自检项,并在出现多副本时给出可执行的修复指引。另需提醒使用者的一个副作用:
0.2.0-rc.2的降级策略意味着非必需插件会静默降级——实例照常服务、界面照常可用,若不看 stderr 就会以为一切正常,而实际上某条插件(例如沙箱能力)并没有起来。建议在文档或 CLI 提示里明确"warning 需要留意",或提供一个查询"当前有哪些条目未激活"的命令。环境
@deepseek-ai/dsh;版本0.1.5-rc.2(既有树 + 全新装各一次)、0.2.0-rc.2(全新装)<npm 全局前缀>\node_modules\@deepseek-ai\dsh0.1.5-rc.2树 4 份;全新0.1.5-rc.21 份;全新0.2.0-rc.21 份(均为落盘后 glob 实测)0.1.5-rc.2树)0.2.0-rc.2)app.asar归档内,我们没有在该归档内部核对副本数,故不对成因作断言)English body
Summary
On Windows, an npm-installed
dshcan fail to boot with no error text at all — and the behaviour differs by version, as measured on this machine:0.1.5-rc.2(existing install tree): the process prints its web URL, then exits with code 1. The only message is the one quoted below — and nothing was "logged above". The real cause is aDuplicate type name 'DSH_STARTUPINFOW'thrown while importing@deepseek-ai/dsh-sandbox-local, and the loader swallows it.0.2.0-rc.2(fresh install): the same failing entry no longer takes startup down — the instance keeps serving (URL printed, HTTP 303) and only two stderr lines appear:dsh: warning: 1 entry did not activateplus<id> (<name>): failed to import. The thrown exception itself is still not shown.Two separate problems are worth fixing upstream: (1) a dependency-tree defect — on the affected existing
0.1.5-rc.2tree,@deepseek-ai/dsh-win32-processexists as four physical copies, and loading two of them in the same process collides in koffi's global type registry; (2) a diagnosability defect — a failed entryimport()leaves the loader entry with no fiber and its exception goes only toctx.logger.error, so on0.1.5-rc.2it surfaces as "could not be resolved", and on0.2.0-rc.2as a barefailed to importwith no reason.1. Reproduction
Precondition — this is a property of the install tree, not of the version. The failure was observed on an existing
0.1.5-rc.2tree on this machine, in which@deepseek-ai/dsh-win32-processexists in four physical copies. To separate the two variables we also measured fresh installs into empty directories:@deepseek-ai/dsh-win32-processcopies (globbed after install)0.1.5-rc.2treenpm install @deepseek-ai/dsh@0.1.5-rc.2 --prefix <fresh-dir>npm install @deepseek-ai/dsh@0.2.0-rc.2 --prefix <fresh-dir>So: a fresh
0.1.5-rc.2install does not reproduce the four copies; the affected tree appears to have accumulated them over multiple installs/upgrades.Observed there: prints
dsh web: http://127.0.0.1:<port>/?token=…, then exitscode=1with the message quoted above. No profile-specific configuration is involved — a brand-new profile created with--from-default-profile webfails identically, and so does the pre-existingwebprofile.To isolate the diagnosability behaviour from the dependency-tree defect, we mounted a trivial plugin that simply throws at import time (top-level
throw new Error(...)) via a--patchoverlay, and ran it against both versions:0.1.5-rc.20.2.0-rc.21Error: dsh: plugin tree failed to load: … plugin(s) failed to load: @deepseek-ai/dsh-sandbox-local, <probe>; Cordis startup failed because these plugin(s) could not be resolved (see the error(s) logged above)dsh: warning: 1 entry did not activate+<probe> (file:///…): failed to import2. Root cause: duplicate
dsh-win32-processcopies → koffi type collision@deepseek-ai/dsh-win32-process/lib/index.jsregisters a koffi struct at module evaluation time:koffi's type registry is process-global. On the affected existing
0.1.5-rc.2tree, this package exists at four paths (measured with a glob over that install tree):Two distinct copies therefore evaluate in the same process, and the second one throws:
dsh-sandbox-localstatically importsdsh-sandbox-windows-acl, which statically importsdsh-win32-process, so thesandboxrow always pulls in one copy; whichever path pulls in another copy first determines the outcome. We could not pin down the exact second loader: disabling thesubprocessrow did not prevent the collision, because that package is also reachable through ordinaryimportchains.Both fresh installs measured here contained exactly one copy (see the table in §1), at
<install-root>/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-win32-process(0.1.5-rc.2) respectively<install-root>/node_modules/@deepseek-ai/dsh-win32-process(0.2.0-rc.2) — i.e. this is a tree-state problem, not a version-inherent one.3. Why the user sees nothing: the loader swallows the exception
@deepseek-ai/cordis-plugin-loader/lib/index.js(Entry._init, lines 448–462):On
0.1.5-rc.2(measured):@deepseek-ai/dsh-app-bootthen audits the settled tree (assertEntriesLoaded, lines 1434–1440) and reports every entry withfiber === undefined && !disabledas "could not be resolved". The message points at "the error(s) logged above", butctx.logger.erroroutput never reached stdout/stderr — and the follow-up audit that would explain things (assertEntriesActivated, lines 1465–1488, which reports pending entries' missing services) is never reached, becauseassertEntriesLoadedthrows first.--dump-configpasses, so static checks cannot catch it either.On
0.2.0-rc.2(measured + source): that audit has been rewritten.assertEntriesLoaded/assertEntriesActivated, along with the stringscould not be resolvedandsee the error(s) logged above, no longer exist in this version;auditStartupEntries(:4009-4020) instead decides fatality from a required listrequiredStartupEntryIds(:3836-3844— onlyagent-loop,webserver,modules,connection,headless-runner,acp,sdk-jsonrpc-server): entries in that list failing cause athrow; everything else only produces a warning, and successful siblings keep running. The wording became per-entry:N entry did not activate+<id> (<name>): failed to import. The cost: for the "no fiber" caseinactiveEntrieswrites a hard-coded string"failed to import"(:3905-3913) and does not carry the exception swallowed byEntry._init()— which is exactly why the thrown message is still invisible (measured: our injected marker appeared in neither stdout nor stderr).4. How we got the real error (workaround others can reuse)
Mount a tiny diagnostic plugin via
--patchand, inside itsapply(): (1) wrapctx.logger.errorto also write tostderr; (2) callctx.loader.import(name, () => [])directly — that entry point rethrows instead of swallowing. That immediately surfacedDuplicate type name 'DSH_STARTUPINFOW'. (--patchis a top-level dsh option and must precede the pass-through web-app flags.)5. Suggested fixes (upstream) — updated against
0.2.0-rc.20.2.0-rc.2: failures are now per-entry visible and no longer fatal. Still missing: the warning states only the hard-codedfailed to import, without the real exception. Please propagate the errorEntry._init()catches (rethrow it, or at least append it to the warning), so "which plugin" and "why" are both visible.@deepseek-ai/dsh-win32-processto a single physical copy per install tree — the multi-copy state is not version-inherent (measured): fresh installs of both0.1.5-rc.2and0.2.0-rc.2into empty directories each contained exactly one copy, while the affected tree had accumulated four. Suggest an install/upgrade self-check for "copies ofdsh-win32-processin this tree == 1", with actionable repair guidance when it fails.One side effect worth documenting:
0.2.0-rc.2's policy means non-required plugins degrade silently — the instance serves normally, so a user who does not read stderr will assume everything is fine while some capability (e.g. the sandbox) is not actually up. Consider marking these warnings as attention-worthy in the CLI, or exposing a way to query which entries are inactive.6. Environment details
@deepseek-ai/dsh;0.1.5-rc.2(existing tree + one fresh install) and0.2.0-rc.2(fresh install)<npm global prefix>\node_modules\@deepseek-ai\dsh0.1.5-rc.2tree: 4; fresh0.1.5-rc.2: 1; fresh0.2.0-rc.2: 1 (all globbed from disk after install)0.1.5-rc.2tree)webprofile both fail0.2.0-rc.2)app.asararchive and we did not inspect copy counts inside that archive, so no claim is made about the causeAll reactions