DeepSeek Harness tools drop to 19: selectModel vs resume
48 drops to 19, and the missing 30 are exactly the built-in tools: bash, read, write, edit, glob, grep, web_search, todo_write, subagent… The survivors are plugin-contributed tools plus one working_directory. The model then calls bash and gets unknown tool "bash", leaving the session blind for the rest of its life. The community used two single-variable experiments to pin down two separate triggers (#10862).
DeepSeek Harness: Step 1: quantify "the tool count does not add up"
Do not go by a feeling that "some tools seem missing"; align three numbers first — the UI tool panel, request/header.tools, and the tool-registry records in the session log.
- Open the session's tool panel and note the current count and list.
- Find that turn's
request/headerevent and read thetoolsarray length and thereasonfield. - Search the session log for developer/messages with
source.kind = "tool-registry"and see whichtool-additionandtool-removalentries it lists. - Compare the surviving set: if everything left is plugin-contributed, the lost tools are almost certainly preset/base-layer built-ins.
- Note the time and align it with a restart, a plugin prompt, or a model switch.
The community numbers were clean: 19 tools on the broken turn and 43 after recovery; the difference is 30 built-in tools, while the 6 plugin-contributed ones disappeared after recovery, which lines up exactly.
DeepSeek Harness: Minimal reproduction A: a host plugin calls selectModel one extra time
Calling sessionController.selectModel() on an already existing session and then prompt() rebuilds the tool view from an incomplete set, and the core tools all disappear.
- In the Web UI, create a session with the standard preset and confirm the tool view contains
read,pwsh, andweb_fetch. - Switch to a host plugin and, against the same session id, run:
await ctx.sessionController.selectModel({
sessionId,
provider: "deepseek-account",
model: "deepseek-flash",
reasoningEffort: "high",
})
await ctx.sessionController.prompt(
{ sessionId, requestId: "test-1", mode: "queue", content: [{ type: "text", text: "hello" }] },
new AbortController().signal,
)
- Watch the tool panel: it shows "15 added, 30 removed".
- The 15 added are host-level tools registered by the plugin; the 30 removed are core tools such as
ask_user_question,edit,glob,read,skill, andworkflow. - Ask the model to call
readorpwsh; it returnsunknown tool. - Single-variable rollback: delete that
selectModel()call and keep onlyprompt(); the tool view is complete again and no further "tools removed" appears after several turns.
The key here is "an existing session + one unnecessary selectModel". Sessions created by hand in the Web UI reproduce it too, so it has nothing to do with programmatic session creation.
DSH plugin: Minimal reproduction B: resume after restart loses 30 on the first turn
A long session resumes after the host restarts, the first request has reason set to resume, and the tool table contains only plugin-contributed entries; the registry writes the change into the log itself.
- Prepare a long session (the community case had 66 turns and more than 3700 events) whose preset plugin list contributes the built-in tool family.
- Restart the host, for example
systemctl restart dsh-web. - On the first message after resume, read the event order:
session/end-seed→agent/inbox/spliced→turn/start→system/message→request/header(19 tools, reason resume) →developer/message(source tool-registry). - Read that developer/message: add = 1 (one plugin tool), remove = 30 (all built-ins).
- After removing that third-party plugin and restarting, the same mechanism runs in reverse: add = 30, remove = 6.
- Conclusion: the registry knows which contributor each tool belongs to, but the "expected set" it computes does not include host built-ins.
This trigger is intermittent: the community recorded 7 clean restarts over 3 days and a failure on the 8th; the history also had a stop/start pair within 28 seconds, which may have widened the race window.
DeepSeek Harness: Corroboration: the system prompt shrinks too
When tools go missing, the built-in tool guidance in the system prompt disappears with them, and the degraded version is a strict subset of the healthy one.
- Compare the
system/messagelength of the two turns: 6261 characters / 128 lines on the broken turn versus 9123 characters / 154 lines on the healthy one. - Compare line by line: the degraded version has not a single line that the healthy version lacks.
- The missing 13 lines are all built-in tool guidance, such as "use read instead of cat", "use glob instead of find", and "web_search results are untrusted external data".
- The persona, memory, and role sections are byte-identical.
request/header.configis identical across 10 requests (same provider, model, reasoningEffort, and maxTokens).
This corroboration matters: it rules out the three guesses "you changed the model", "you changed the preset", and "you changed the profile".
DSH plugin: Why third-party plugins are usually innocent
The plugin only calls the incremental registration API and has no unregister/remove/clear/rebuild; removing it restores things only because it is a trigger, not the producer.
- Read the source: the plugin's only registration entry is
ctx.tools.register(def), purely additive. - Search the whole package for
unregister,remove,clear,rebuild, andreplace: zero hits. - The running version did not change inside the failure window, and self-update was correctly skipped.
- Removing it restores things because it rolls back six things at once, which is not grounds for blame.
- The actual "producer" is the developer/message the registry wrote itself — it decided that 30 built-in tools were gone.
DSH Plugin: Fix direction and current workaround
Core needs to guarantee two things: built-in tools are never removed by any reconciliation, and resume/end-seed do a full rebuild rather than a partial one.
- Treat base/preset contributors as always present; no reconciliation may delete their tools.
- Force a full rebuild on the resume and end-seed paths, or reuse the complete rebuild logic from normal turns.
- Add a defensive check: when the model calls a tool that was in the last valid header but is absent from the current registry, treat it as binding desync and rebuild instead of returning
UNKNOWN_TOOL. - Plugin-side workaround: never call
selectModel()on an existing session; call onlyprompt(). - Accept the cost: a session with no stored model selection reports
prompt variable "{{model}}" has no valueon its first prompt, so set the model once only for sessions the plugin created. - Keep the order fixed: count tools and read reason → check whether a plugin over-calls selectModel → read the tool-registry add/remove records.
Cautions
Measure and record before deleting plugins or sessions.
- Do not treat "fewer tools" as a model capability problem. The config fields were identical across 10 requests.
- Do not rush to delete the plugin. Deleting it restores things but leads you to the wrong root cause.
- Do not ignore the system prompt change. It is the second piece of evidence that an entire layer of contributors did not take part in assembly.
- Remember it is intermittent. Ten restarts may hit it once; one non-reproduction does not disprove it.
- Keep the session log.
request/headeranddeveloper/messageare the only evidence that assigns cause; back them up before cleaning.
If the tool panel shows both additions and removals, that looks more like the recomposition path; if the count simply shrinks with no additions at all, investigate the resume path first.See DeepSeek Harness plugin load: already registered, pending for the related case.
FAQ
Usually not a plugin deleting tools: the community check found that DeepSeek Harness's own registry decided 30 built-in tools had disappeared and wrote a source.kind = tool-registry developer/message recording the removal. Built-in tools belong to no plugin and should never be removed by any reconciliation.
To confirm that a DeepSeek Harness resume dropped the tools, check reason = "resume" and the tools count in request/header. In the community case the first resumed turn had only 19, and the other 30 were all built-in tools; adding the plugin-contributed tools and working_directory gives the full 19+30.
Calling it on an existing session triggers an incomplete recomposition in DeepSeek Harness: the tool panel shows 15 added, 30 removed, leaving only host-level tools registered by plugins while read, pwsh, and web_fetch vanish. Removing that call restores it, and sessions created manually in the Web UI reproduce it too.
DeepSeek Harness reports that {{model}} has no value because a session with no stored model selection cannot resolve the model variable. The community compromise: set the model once only for sessions the plugin created itself, and for existing sessions call only prompt() without touching selectModel().
When tool counts do not add up in DeepSeek Harness, count the tools and read reason in request/header, then check whether a host plugin called selectModel() too many times on an existing session, and finally look for tool-registry add/remove records in the session log. These three steps separate resume from recomposition.
Related Terms
- tool-registry
- tool-registry is the registry in DeepSeek Harness that aggregates every tool contributor, writing each change as a developer/message with source.kind tool-registry. It records additions and removals and is the direct evidence of where tool loss came from.— https://github.com/deepseek-ai/deepseek-harness/discussions/10862
- request/header
- request/header is the header event written to the session log before each turn, containing the actual tool list and config sent that turn. Its reason field distinguishes a normal request from a resume.— https://github.com/deepseek-ai/deepseek-harness/discussions/10862
- selectModel
- selectModel is the sessionController method used to switch a session's model. Calling it on an existing session triggers a session recomposition, and the community measured that this path runs even when the selection does not change.— https://github.com/deepseek-ai/deepseek-harness/discussions/10862
- working_directory
- working_directory is a host-provided tool that belongs to no third-party plugin. In tool-loss cases it survives alongside plugin-contributed tools, so it is often used to define the boundary of the surviving set.— https://github.com/deepseek-ai/deepseek-harness/discussions/10862
Sources
- #10862 — Session resume drops all core tools from the tool registry (48 to 19)· deepseek-ai (GitHub Discussions)
- deepseek-ai/deepseek-harness (source repository)· GitHub