Fix "Cannot read properties of undefined (reading 'prepare')" in DSH
DeepSeek Harness throwing Cannot read properties of undefined (reading 'prepare') on every tool call usually means a second copy of @deepseek-ai/dsh-tools sits inside the profile — the tool scheduler is keyed by a module-local Symbol(), the two copies never share the key, and the lookup comes back undefined. One find command locates it, cleanup restores the profile; but sessions that already carry dangling tool_calls cannot be recovered and must be abandoned.
What the DeepSeek Harness "reading 'prepare'" error looks like
The error is always UNKNOWN: Cannot read properties of undefined (reading 'prepare'), it fires on every tool call, and nothing fails at load time. A user hit it in practice (discussion thread) with these symptoms:
- The profile starts normally, but any tool call throws
Cannot read properties of undefined (reading 'prepare'); - The error names neither the failing tool nor the duplicated package — the natural first guess is the framework or the model provider;
- Worse, there is a secondary injury: the failed turn writes
tool_callsinto the log without results, and the next request is rejected by the model provider (An assistant message with 'tool_calls' must be followed by tool messages...), which makes the whole session unrecoverable.
This error is hard to debug because the problem hides in dependency resolution, not in runtime logic: TOOL_RUNTIME_SCHEDULER is a module-local Symbol() (defined in dsh-tools), and Symbol() identity is isolated per module instance — the reader and the writer must resolve to the same dsh-tools file. As soon as the profile holds a second copy, the two sides no longer share the key (source).
Why a DSH plugin can install two copies of a core package
A duplicated core package has three sources: a plugin declaring core packages as direct dependencies, a packed tarball dragging in transitive copies, and stale node_modules from an earlier install. One by one:
- A plugin declares
@deepseek-ai/*as direct dependencies: the correct place ispeerDependencies, so the dependency tree resolves exactly one copy; asdependenciesit installs a second copy into the profile; - Installing from a packed tarball: installing the DSH binary as a packed tarball most easily drags in duplicated transitive copies, especially when a package declares an over-wide peer range;
- Stale node_modules: the most hidden one — the reporter's headless profile had an empty dependency list and the offending plugin was not in bundles either, yet an old
node_modules/@deepseek-ai/tree was still there and tool calls failed every round. Moving the leftover directory away restored it immediately (mv ~/.dsh/profiles/headless/node_modules/@deepseek-ai /tmp/backup/).
So "uninstalling the plugin" is not enough — as long as a copy still lies in node_modules, the profile stays broken, and no configuration tells you why.
How to find the duplicate core package: one find command
The check is a single find: the profile's node_modules must not resolve any @deepseek-ai core package — the core comes from the CLI's own dependency tree, the profile should only hold plugins. Step by step:
- Locate the profile directory and confirm the profile name:
ls ~/.dsh/profiles/
- Check for a dsh-agent copy — it should print nothing:
find ~/.dsh/profiles/<name>/node_modules -maxdepth 4 -path '*/@deepseek-ai/dsh-agent' -print
- Check dsh-tools the same way — again nothing:
find ~/.dsh/profiles/<name>/node_modules -maxdepth 4 -path '*/@deepseek-ai/dsh-tools' -print
- Output means two copies are installed: note the found directories, delete them or move them to a backup folder:
mv ~/.dsh/profiles/<name>/node_modules/@deepseek-ai /tmp/dsh-backup/
- Restart dsh (
Ctrl+C, thendsh webagain), and in a new session ask the Agent to call a tool — no morereading 'prepare'means it is fixed.
Verified by the reporter: moving the whole
node_modules/@deepseek-aidirectory (instead of package by package) cleans it up in one go; rerun the same command withdsh-agent/dsh-toolsto confirm nothing remains.
Fix and prevention: cleaning duplicates and DSH plugin dependency conventions
The fix has two layers: upstream should switch the key to a cross-copy Symbol.for() (with a protocol version guard), and plugin authors should put core packages in peerDependencies. The community confirmed the root cause and suggested this direction (source):
- Switch the key to
Symbol.for('@deepseek-ai/dsh-tools.scheduler'): both copies then compute the same key, and a hard failure degrades into "merely a redundant install"; - Add a protocol version guard:
Symbol.foralone only fixes access — copies of different versions share the same key, and if the scheduler shape changes across versions, a loud crash turns into a silent mismatch. The registrar should stamp a version, and the reader should validate it and report both versions; - Fail loudly at load time: when composing a profile, run a realpath check on each
@deepseek-ai/*package and error out if one package resolves to multiple paths, instead of waiting for a runtime explosion; - Plugin author contract: dsh-context is the correct example (
dependencies: {}, all@deepseek-ai/*plus react/zod inpeerDependencies). Official docs should pin this down, anddsh plugin addshould warn when a plugin declares a core package as a non-peer dependency.
Until the upstream fix lands, the find-and-clean in this article restores the profile. This debugging story is also a reminder that the trustworthiness of the plugin source matters — human-verified plugins in the catalog produce far fewer of these packaging problems. If you would rather not wrestle with the dependency tree by hand, the desktop app's built-in DSH Plugin Hub (dsh-plugin.org) manages installed plugins centrally — what is installed is visible at a glance, and uninstall-and-cleanup takes one click.
Notes
- The error appears when a second core package exists — run find first before suspecting the model or the framework, and do not burn time in the wrong direction.
- A new session is mandatory after cleanup: the old failed session already carries tool_calls without results, and the model provider keeps rejecting it.
- Check every profile when you have several; residue in any one of them kills tool calls for that profile.
- Similar plugin errors are collected in the DeepSeek Harness plugin error collection: DSH plugin not loading, Web UI issues, and session cache repair.
FAQ
A second copy of @deepseek-ai/dsh-tools exists in the profile's node_modules. The tool scheduler uses a module-local Symbol() as its key, the two copies never share the key, and the agent loop resolves the scheduler to undefined when it builds a service from the other copy (source).
Most commonly a plugin declares @deepseek-ai/* as direct dependencies (peerDependencies is correct); secondly, installing the CLI from a packed tarball drags in transitive copies; thirdly, stale node_modules left by an earlier install keeps breaking tool calls even when the profile no longer references them.
Use find against the profile's node_modules: find <profile>/node_modules -maxdepth 4 -path '*/@deepseek-ai/dsh-agent' -print should print nothing; repeat with dsh-tools in place of dsh-agent. Any output means a second copy is installed; move it away and restart dsh web to recover.
No. The failed turn leaves tool_calls without matching results, and the next request is rejected by the model provider with "An assistant message with 'tool_calls' must be followed by tool messages". The session must be abandoned and a new one started.
Remove the duplicate. Delete or move away the leftover node_modules/@deepseek-ai copy under the profile (for example mv ~/.dsh/profiles/<name>/node_modules/@deepseek-ai /tmp/dsh-backup/), restart dsh (Ctrl+C, then dsh web again), and tool calls work again in new sessions; already corrupted sessions are abandoned.