Fix DSH agent loop crash after tool call - version drift
DeepSeek Harness crashing mid-session after a tool call with Cannot read properties of undefined (reading 'prepare'), while plain-text replies work fine, usually means version drift: a third-party plugin declares @deepseek-ai/dsh-tools as a direct dependency and resolves an older copy into the profile (measured 0.1.0-rc.8 vs the global 0.1.1-rc.2), so the scheduler's Symbol() key never matches across copies and the agent loop reads undefined and dies. Compare core package versions between the profile and the global CLI, then bisect by disabling plugins to locate and recover. This is the same mechanism as the duplicate core package error but with a different trigger path; this article isolates it.
What the crash after a tool call looks like
The error is always Cannot read properties of undefined (reading 'prepare'), code UNKNOWN, it appears after a tool call, and plain-text turns are completely fine. A reporter reproduced it in practice (discussion thread):
- Session log sequence:
tool/callis emitted, immediately followed bystep/end— notool/resultis ever written — thenturn/endreportsreason.error "Cannot read properties of undefined (reading 'prepare')"; - Reproducible across many turns: three consecutive crashes in the original post (turns 14/15/16), later expanded to 10 consecutive crashed turns;
- Plain-text turns stay healthy (no-tool turns end with
turn/end completed) — the model/provider path is fine, the failure sits in the tool dispatch layer; - Restart/resume restores the session (
reason:"resume"), and the service layer never crashes (curl :3080stays HTTP 200).
One detail in the evidence is the key discriminator: turns 1–13 each paired tool/call with tool/result, so the scheduler was alive at that point. If a static duplicate sat in the profile, the first tool call would have crashed — crashing mid-session points at "the module graph mutated within the process lifetime" or "a second evaluation edge".
Version drift vs duplicate core package: same mechanism, different path
Both share one mechanism — TOOL_RUNTIME_SCHEDULER is a plain Symbol(), not Symbol.for() (definition, measured at lib/index.js:2416); every call mints a unique instance, two copies never share the key, and reading registry[TOOL_RUNTIME_SCHEDULER] across copies returns undefined (#4529 root cause). But the trigger differs:
- Static duplicate (#4640): a leftover
node_modules/@deepseek-ai/tree sits in the profile and the first tool call at boot crashes; - Version drift (this article): a plugin installs a core package as a direct dependency, resolving an older version (
0.1.0-rc.8≠ global0.1.1-rc.2); the session crashes mid-way (from turn 14), and restart/resume or disabling the plugin restores it.
The reporter verified the profile's node_modules/@deepseek-ai/dsh-tools is a real directory, not a symlink, version 0.1.0-rc.8 — same string at lib/index.js:2416 as the global 0.1.1-rc.2 but a different Symbol instance: two physical files make two evaluation edges.
Who pulls an older core package into the profile: the dependency list
Any third-party plugin that declares @deepseek-ai/dsh-tools in dependencies (instead of peerDependencies) is the injection source. The reporter checked each package (source):
dsh-obsidian:"@deepseek-ai/dsh-tools": "*"(the wildcard is the most dangerous — always installs "latest", which may drift);dsh-better-sidebar:"^0.1.0-rc.8";dsh-chat-import,dsh-recall:^0.1.0-rc.8line;"^0.1.0-rc.6"in the measured list;dsh-univer-office:"0.1.0-rc.8"(pinned old version, nailing an older core package into the profile).
pnpm resolving 0.1.0-rc.8 (older than the global 0.1.1-rc.2) forms the second evaluation edge of version drift. There is also a side path: npm install <plugin> (npm >=7 auto-installs the plugin's peer set) copies core packages, while dsh plugin (pnpm, autoInstallPeers: false) does not — the same plugins behave differently depending on the install channel.
Troubleshooting: compare core package versions + bisect by disabling plugins
Two locating tracks: compare core package versions between the profile and the global CLI, then bisect with disabled plugins. Step by step:
- Confirm the model/provider path is healthy: ask the Agent for a plain-text reply; normal output rules out the provider. Then confirm the service stays 200:
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:3080/
- Rule out a static duplicate: open a brand-new session and run the first tool call — if it crashes immediately, follow the duplicate core package article; if it crashes mid-way, continue here;
- Compare versions: check the global CLI version first (
dsh --version), then list the core packages inside the profile and compare one by one:
dsh --version
ls ~/.dsh/profiles/<name>/node_modules/@deepseek-ai/
An older version in the profile (e.g. 0.1.0-rc.8 vs the global 0.1.1-rc.2) means drift;
4. Look for physical copies: use find to confirm whether the profile holds a real directory:
find ~/.dsh ~/.npm-global -type d -name dsh-tools 2>/dev/null
- Bisect by disabling plugins: list the installed plugins, then set the suspected plugin to
disabled: truein the profile config:
dsh plugin --profile <name> list
Restart dsh web, and if tools all work again in new sessions (bash/read/glob return results), the culprit is locked;
6. Full recovery: uninstall the plugin and move the drifted core package copy out of the profile, restart — new sessions work again; abandon the old session that already carries dangling tool_calls.
Verified by the reporter: disabling the third-party vision plugin and restarting restored every tool in new sessions — the plugin's enable/disable toggle is the on/off switch for reproduction, usable for clean A/B validation.
Fix and prevention: Symbol.for and plugin dependency conventions
Two layers: upstream should switch the scheduler key to a cross-copy Symbol.for(), and plugin authors should keep core packages in peerDependencies. The community's recommendations (source):
- Switch the key to
Symbol.for('@deepseek-ai/dsh-tools.scheduler'): both copies compute the same key, and the crash degrades into "merely a redundant install"; - Explicit null check at the read site: when
registry[TOOL_RUNTIME_SCHEDULER]hits undefined, throw an actionable diagnostic (naming the possible local duplicate/drift sources) instead of a bareUNKNOWNTypeError — this investigation cost hours precisely because the error was flattened into UNKNOWN; - Plugin author contract: keep all
@deepseek-ai/*inpeerDependencies, never independencies; avoid"*"wildcards and pinned old versions; - Diagnostic recipe: the crash lives in the web session log, not stderr — parse
session.jsonlforturn/end error, and pairtool/callwithtool/resultper turn (the reporter shared aturn_summary.pyscript for exactly this) to locate it fast.
Until the upstream fix lands, disable/uninstall the injection source per the steps above and you recover. This class of problem shows that a plugin's dependency declarations matter more than its features — if you would rather not audit dependency trees by hand, human-verified plugins in the catalog produce far fewer of these packaging problems. The desktop app's built-in DSH Plugin Hub (dsh-plugin.org) surfaces each plugin's source and the installed list, so disabling and uninstalling is visible at a glance instead of hand-editing profile dependencies.
Notes
- A crashed turn leaves
tool_callswithout results, and the provider may reject that session with 400INVALID_REQUEST(#4549 recovery gap) — do not keep retrying inside the same session. - Disabling a plugin plus restarting changes two variables at once; for a rigorous attribution, first restart without disabling, then disable without restarting, and verify separately.
- The reporter honestly notes it cannot be confirmed retroactively whether the drifted copy already existed at crash time — if you need hard proof, do an A/B in clean environments before/after the crash.
- Similar plugin errors are collected in the DeepSeek Harness plugin error collection: DSH plugin not loading, Web UI issues, and session cache repair.
Sources: Discussion #4601, #4529 (root cause confirmed), packages/core/tools, packages/core/agent-loop
FAQ
A third-party plugin declares @deepseek-ai/dsh-tools as a direct dependency (dependencies instead of peerDependencies), and installing it into the profile resolves an older copy (measured 0.1.0-rc.8, older than the global CLI's 0.1.1-rc.2 from dsh --version). The two copies each mint a plain Symbol() scheduler key that never matches, and the agent loop reads undefined across copies (source).
Same mechanism (Symbol() is not robust to duplicate loading) but a different trigger: a static duplicate leaves a whole tree in the profile and fails on the first tool call at boot; version drift installs an older core package as a direct dependency, crashes mid-session (around turn 14), and recovers after restart/resume or disabling the offending plugin.
Three steps: open a brand-new session and see whether the first tool call crashes (crash = static duplicate, see the other article); compare core package versions between the profile and the global CLI (dsh --version, then ls ~/.dsh/profiles/<name>/node_modules/@deepseek-ai/); then run find ~/.dsh ~/.npm-global -type d -name dsh-tools 2>/dev/null to look for physical copies.
Any plugin that writes @deepseek-ai/dsh-tools into dependencies. Measured offenders: dsh-obsidian ("*"), dsh-better-sidebar ("^0.1.0-rc.8"), dsh-chat-import / dsh-recall ("^0.1.0-rc.6"), dsh-univer-office (pinned "0.1.0-rc.8"). npm >=7 also auto-installs a plugin's peer set and copies core packages; dsh plugin (pnpm, autoInstallPeers: false) does not.
First disable the suspected plugin (disabled: true in the profile) and restart dsh web to confirm tools recover; then uninstall the plugin and remove the drifted core package copy from the profile. If the crashed turn left tool_calls without results, the provider may reject the session with 400 INVALID_REQUEST — start a new session.
Sources
- deepseek-harness Discussion #4601: agent loop crashes after bash tool call, 3 consecutive turns report reading 'prepare'· deepseek-ai (GitHub Discussions)
- deepseek-harness Discussion #4529: --profile headless crashes on any tool call, root cause confirmed (TOOL_RUNTIME_SCHEDULER is a plain Symbol())· deepseek-ai (GitHub Discussions)
- deepseek-harness packages/core/tools (where TOOL_RUNTIME_SCHEDULER is defined)· deepseek-ai
- deepseek-harness packages/core/agent-loop/src/tool-calls.ts (where the scheduler is read)· deepseek-ai