Fix DSH agent loop crash after tool call - version drift

TroubleshootingPublished 2026-08-27Author: DSH Plugin Hub
DeepSeek HarnessDSH pluginreading prepareversion drifttool call crash
DSH crashes on tool calls reading 'prepare'? A plugin installs an old @deepseek-ai/dsh-tools in profile (Symbol mismatch). Compare versions, disable plugins.

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):

  1. Session log sequence: tool/call is emitted, immediately followed by step/endno tool/result is ever written — then turn/end reports reason.error "Cannot read properties of undefined (reading 'prepare')";
  2. Reproducible across many turns: three consecutive crashes in the original post (turns 14/15/16), later expanded to 10 consecutive crashed turns;
  3. 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;
  4. Restart/resume restores the session (reason:"resume"), and the service layer never crashes (curl :3080 stays 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 ≠ global 0.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):

  1. dsh-obsidian: "@deepseek-ai/dsh-tools": "*" (the wildcard is the most dangerous — always installs "latest", which may drift);
  2. dsh-better-sidebar: "^0.1.0-rc.8";
  3. dsh-chat-import, dsh-recall: ^0.1.0-rc.8 line; "^0.1.0-rc.6" in the measured list;
  4. 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:

  1. 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:
bash
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:3080/
  1. 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;
  2. Compare versions: check the global CLI version first (dsh --version), then list the core packages inside the profile and compare one by one:
bash
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:

bash
find ~/.dsh ~/.npm-global -type d -name dsh-tools 2>/dev/null
  1. Bisect by disabling plugins: list the installed plugins, then set the suspected plugin to disabled: true in the profile config:
bash
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):

  1. 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";
  2. 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 bare UNKNOWN TypeError — this investigation cost hours precisely because the error was flattened into UNKNOWN;
  3. Plugin author contract: keep all @deepseek-ai/* in peerDependencies, never in dependencies; avoid "*" wildcards and pinned old versions;
  4. Diagnostic recipe: the crash lives in the web session log, not stderr — parse session.jsonl for turn/end error, and pair tool/call with tool/result per turn (the reporter shared a turn_summary.py script 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

  1. A crashed turn leaves tool_calls without results, and the provider may reject that session with 400 INVALID_REQUEST (#4549 recovery gap) — do not keep retrying inside the same session.
  2. 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.
  3. 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.
  4. 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

Why does DeepSeek Harness crash mid-session after a tool call with Cannot read properties of undefined (reading 'prepare') while plain-text replies work?

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).

How is the mid-session reading 'prepare' crash different from the duplicate core package error at boot?

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.

How do I confirm version drift instead of a static duplicate with dsh --version, ls and find commands?

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.

Which third-party plugins pull an older @deepseek-ai/dsh-tools core package into the profile?

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.

How do I recover a session broken by version drift — disable the plugin and clean the duplicate copy?

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