Replies: 1 comment
|
已把这批 本帖属于其中的
本地 CLI 聚焦测试 16/16、tools scheduler key 测试和 CLI/tools/agent-loop TypeScript build 已通过。详细补丁、用户恢复步骤、会话处理和回归门禁见上面的集中回复。 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
On
dsh-v0.1.6-alpha.2(ddefc45fbc, current master tip), a source launch throughtsx— the vector the repo's owndshscript uses — fails on every tool call:The built launch (
node apps/cli/lib/bin.js) is unaffected at the same commit. The two module instances are created by the launcher itself from a single package directory: no duplicate package on disk, no community plugin involved.This trigger is new in 0.1.6-alpha.2, but the symptom is not — it is the same
reading 'prepare'crash that has been reported since August under two other triggers. I have tried to collect everything into this one post, including material from the other threads, so the next person starts from one place instead of five.Most of the depth below is not mine. Credit to #6967 (LLKernel) and the macOS probe in its comments (DreamStan), #6974 (TRAVI5TY), and the August threads by leether (#2078, #2130) and zoahdev.
Flagging a few people who may have context, since the failure mode makes this hard to diagnose from the error alone:
9ddef327a4("feat: resolution mode link to runtime"), which is yours; you also committed therelease(dsh): 0.1.6-alpha.2commit.dsh-v0.1.6-alpha.2release commit (PR Bug: "Fetch available models" returns stale bundled catalog for built-in providers (e.g. OpenRouter) instead of querying the live endpoint #4469), the version this regressed in.7c9bb5914c("refactor(ptc): align runtime packages and services with PTC naming") shaped the scheduler and service contract that now throws, so you may have a view on the intended direction.No blame intended — the split is subtle and the error message points at the wrong thing entirely. What I'd most like to know is which fix direction you'd prefer.
If you are hitting this right now
apps/cli/src/profile-boot.ts:263to?? 'link'(§ Workarounds). Takes effect immediately undertsx; no rebuild.node apps/cli/lib/bin.js webis unaffected.tsxsource launches. If you see the same message there, you are probably in the other trigger family — a second physical copy of@deepseek-ai/dsh-tools.Environment matrix
The same crash at the same commit, reported on three platforms by three people:
tsxsource + builttsxsourcetsxsourceNot platform-specific.
Reproduction, and the A/B that isolates it
Verified here on a pristine checkout at
ddefc45fbc— zero local modifications,git status --porcelainempty. Same tree, same command each time; the only variable isapps/cli/src/profile-boot.ts:263:node --import tsx/esm apps/cli/src/bin.ts --profile headless \ "Read package.json and reply with only the value of its name field. Do nothing else."That task needs exactly one
readcall. (If theheadlessprofile does not exist yet, initialize it from the shipped template with--from-default-profile headless.)resolutionModetsxruntime(new default)dsh: UNKNOWN: Cannot read properties of undefined (reading 'prepare')tsxlink(previous default)@deepseek-ai/dsh-rootprintednode apps/cli/lib/bin.js(same task)runtimeSo
runtimeis not broken in itself — it is fine whenever the whole graph comes fromlib/. It is thetsx+runtimepairing that splits the package, and it splits it deterministically.On the boundary: every session log on my install back to 2026-09-13 had zero occurrences of this message; after the
0d1f50007f → ddefc45fbcmove, 8 of 8 attempted tool calls failed — across bothreadandpwsh, including a brand-new session dying on its very first tool call at turn 1, step 1. Not tool-specific, not tied to pre-existing session state.The raw evidence
A crashed turn, straight out of
session.v3.jsonl.zstd(ids trimmed):Two details are worth reading off this:
tool/callis durably recorded before the throw, andstep/endcloses the step anyway. The events line up exactly withtool-calls.ts:appendToolCall(line 168) runs, then.prepare()(line 170) throws. Theturn/endrecords the failure and nothing else.tool/resultfollows — which is what poisons the session (§ Consequence).(If you are reading logs yourself: these files are concatenated zstd frames, and
zlib.zstdDecompressSyncsilently returns only the first frame — 254 bytes out of 1.35 MB in my case. Decode withscanZstdFramesfrompackages/session/session-persistence-jsonl/src/zstd.ts, or every crash looks like it never happened.)The two probes — seconds, no model request
Both from #6974, and both worth running before anything else. They show the two planes independently:
Two planes, one process. This is also a ready-made smoke test — see § Questions.
Where it throws, and why the message misleads
packages/core/agent-loop/src/tool-calls.ts:170:ctx.toolsresolves, butctx.tools[TOOL_RUNTIME_SCHEDULER]isundefined. That key is a module-private symbol, not a string:packages/core/tools/src/index.ts:463Symbol()is notSymbol.for(), so it is only equal across a process while the package is loaded exactly once. Two instances produce two symbols, the lookup is silentlyundefined, and the property read is what throws.The message is why this family has been re-diagnosed five times. Nothing checks that the scheduler exists, so what surfaces is
Cannot read properties of undefined (reading 'prepare')— naming a method on a value that was never there, and pointing atagent-loopwhen the fault is in module identity. Every reporter so far has had to rediscover that the real question is "how many copies ofdsh-toolsare alive in this process". #2130's "Diagnosis" row calls this the third unclosed feedback loop, and I think that is exactly right.For reference, the build output does contain two independent definitions of the symbol:
packages/core/tools/lib/index.js:2526—const TOOL_RUNTIME_SCHEDULER = Symbol("@deepseek-ai/dsh-tools.scheduler");packages/core/tools/lib/types/index.js:51—export const TOOL_RUNTIME_SCHEDULER = Symbol('@deepseek-ai/dsh-tools.scheduler');packages/core/tools/lib/types/ptc.js:13imports it from./index.js, so that subtree is internally consistentThe change that introduced it
9ddef327a4"feat: resolution mode link to runtime" flipped the non-packaged default inapps/cli/src/profile-boot.tsandapps/desktop-host/src/index.ts:linkmaterialised junctions on disk and left resolution totsx;runtimeinstalls an in-process resolver (it reaches Node's internal ESM/CJS loader throughnode-addon-require-builtin; seepackages/boot/app-boot/src/profile-resolution/resolver.ts) and mounts the plugin tree fromlib/. Both planes are then live in one process, andtsxkeeps re-projecting workspace imports tosrc.The tool-call code itself did not change. Between
dsh-v0.1.6-alpha.1(where tool calls worked) anddsh-v0.1.6-alpha.2, the only changes insidepackages/core/agent-loop/srcandpackages/core/tools/srcareinbox.ts(−5/+2, inbox projection registration dedup) andagent-loop/src/index.ts(+2).tool-calls.tsandtools/src/index.tsare byte-identical between the two tags. This is a module-identity regression, not a logic one.Nor is the phenomenon new in kind: #2091 already documented source launches loading workspace packages twice, splitting a different registry. What is new is that it now reaches a
Symbol()-keyed contract, which turns a previously silent split into a fatal one.Independent confirmations of the split
LLKernel captured it directly ([Bug] 0.1.6-alpha.2 source launch (pnpm dsh) splits @deepseek-ai/dsh-tools across src/lib — every tool call fails with "Cannot read properties of undefined (reading 'prepare')" #6967), with
module.registerload hooks in a single boot:DreamStan confirmed the symbol identity in-process on macOS ([Bug] 0.1.6-alpha.2 source launch (pnpm dsh) splits @deepseek-ai/dsh-tools across src/lib — every tool call fails with "Cannot read properties of undefined (reading 'prepare')" #6967 comment):
ctx.tools[<lib-plane symbol>]is defined,ctx.tools[<src-plane symbol>]is not, andsrcSymbol === libSymbolisfalse. No model involved, so it cannot be a provider-protocol artifact. Their resolution matrix showstsxprojecting the bare specifier tosrcfrom every parent, while the runtime generation mounts fromlib— the split is in the process, not in any one importer.TRAVI5TY measured both planes on Linux (0.1.6-alpha.2 regression: a source checkout now loads harness packages from both src/ and lib/, crashing the first tool call and leaving the session unresumable #6974): both probes above, plus the conclusion that the packages are single copies on disk — the duplicate is the module instance, not a directory. That is why none of the plugin-side remedies in Pitfall map for DSH plugin authors: the 'reading prepare' crash is a duplicate @deepseek-ai/* copy — three guards that would catch it #2130 apply to this trigger.
Three platforms, same commit, same one-line cause.
The report family — same symptom, three different triggers
Worth separating, because the remedies differ:
pnpm installin the profile, e.g. via the plugin-manager UI)src/andlib/— newThe existing remedies for the plugin/profile-copy family (#2130's guidance) do not apply here — this trigger has no duplicate on disk and no third-party plugin involved.
Consequence: the session is left unresumable
This deserves its own heading, because it is easy to miss and it is what confuses users most.
When the dispatch fails, the assistant message and its
tool/callevents are already durably recorded, and no results are ever written. Every later turn replays that history, and the provider protocol rejects it during serialization — before any HTTP request:packages/llm/llm-deepseek/src/protocols/messages/serialize.ts:117So the damage is two-stage: one failed turn, then a session that can never continue. #6971 is exactly this seen from the UI — a new session reports
reading 'prepare', an old session reportstool calls need immediate results, which reads like "even downgrading doesn't help".As TRAVI5TY points out (#6974), this is by construction rather than bad luck: agent-loop drains dispatches on a scheduler failure without committing synthetic recovery results, whereas cancellation writes
ABORTED_BEFORE_DISPATCHpairs. Any dispatch failure therefore poisons the session, not just this one. A synthetic result pair for a failed dispatch would reduce this entire class to a single failed turn — separately worth doing, whatever happens to the resolution question.LLKernel also worked out a repair for already-damaged logs (#6967 comment): truncate each log after the last
tool/callof the damaged step, so the built-in crash repair closes that turn on the next resume. Verified against the real serializer there: the original log is rejected withhistory ends with unresolved tools, the repaired log serializes clean with every tool call paired to a result; only the failed turn's own artifacts are lost.There is no escape hatch
Also from #6974, and worth knowing before you go looking:
parseDshArgshas no resolution-mode flag,runClicallsrunProfilewithoutresolutionMode, and nothing reads an environment variable for it. A source-checkout user cannot selectlinkwithout editing source. It also makespnpm run buildmandatory after every pull, because the packaged rows readlib/.Workarounds
One line, source launches only — verified above:
Under
tsxthis takes effect immediately; no rebuild needed.Run the built launcher:
node apps/cli/lib/bin.js web. Works on Windows and macOS (confirmed in [Bug] 0.1.6-alpha.2 source launch (pnpm dsh) splits @deepseek-ai/dsh-tools across src/lib — every tool call fails with "Cannot read properties of undefined (reading 'prepare')" #6967's comments), including with third-party bundles — at the cost of rebuilding after every source change.A branch already exists: Honghe linked
fix/resolution-mode-link-defaulton [BUG] 更新不能太积极,又不能用了! #6971, which is the same direction.Make the symbol global —
Symbol.for()instead ofSymbol()(Fix: `reading 'prepare'` crash from duplicate `@deepseek-ai/dsh-tools` copies (Symbol-key mismatch) — a one-line `Symbol.for` fix #2078, branchleether/deepseek-harness → fix/tool-runtime-scheduler-symbol). See the trade-offs below; both the original proposer and zoahdev now treat it as a local unblocker.Damaged sessions need separate repair — patching the resolution does not revive them.
Prior proposals, and why they were not adopted
This part matters for anyone thinking about the fix, because the obvious one-liner has already been round-tripped once.
#2078 —
Symbol.for(). The standard dual-package-hazard remedy: the key becomes identical across all module instances while staying aSymbol, preserving the@internalintent. Verified there against duplicate copies on 0.1.0-rc.6.The gap zoahdev raised in #2078's comments — version skew.
Symbol.formakes the key identical but not the value's protocol. If two copies come from different trains (say the host's and an older one hoisted from a plugin), the global key means one copy now reads the other copy's scheduler. If their protocol shapes differ, that is a silent cross-wire — which "works" until it doesn't, and is harder to attribute than the originalundefined. Their branch adds aTOOL_RUNTIME_SCHEDULER_PROTOCOL_VERSIONguard on top.#2130 — the proposer's own revised position. Symbol.for is a defensive stopgap with side effects, not the fix, because it: makes the key semi-public (breaking
@internal), freezes the key string forever, silently cross-wires across versions, and removes the only signal we have (a loudundefinedbecomes a silent success or a silent cross-wire). "In a single-instance world preserved by relink + a check,Symbol()is strictly better."That thread also frames the whole family as two reasonable defaults colliding — a private-identity
Symbol()on one side, npm's "import X → dependencies" default on the other — with three feedback loops never closed: documentation (publish.mdnever says host packages must bepeerDependencies), an install-time check (dsh pluginis a bare pnpm forwarder that silently accepts a plain@deepseek-ai/*dependency), and diagnosis (the crash reportsundefined, not "two physical copies, one from the host and one from plugin X"). It maps five ways a duplicate appears with rough frequency shares — note those are estimates from discussion frequency, not measurements.I find that framing convincing, and it applies to this trigger too: the source-launch split is a sixth path, and none of the three loops would have caught it either.
Two constraints on the fix space
Both come from the other threads, and are worth not rediscovering:
lib/" does not work. DreamStan triedTSX_TSCONFIG_PATHpointed at a paths-less tsconfig; the plugin tree and the launcher graph then do agree onlib, but the source launch dies at import time withSyntaxError: The requested module '@deepseek-ai/cordis' does not provide an export named 'FiberState'.FiberStateis aconst enum(vendor/cordis/src/fiber.ts:147) thattscinlines, sovendor/cordis/lib/index.jscarries no runtime enum object. A source launch must therefore keep thesrcplane for the launcher's own graph — which rules out "everything to lib" and supports the "source → link, built → runtime" direction.Questions
tsxsource-launch path covered by CI? If not, that would explain how this reached a release tag while the built path stayed green. Both probes in § The two probes reproduce it in seconds with no model request, so a smoke test looks inexpensive..prepare?All reactions