DeepSeek Harness Plugin

Leeminjing/dsh-messages-sanitizer

Stars ★ 3 Downloads (30d) 947 Category Sessions & Messages Added 2026-08-15 npm dsh-messages-sanitizer

Auto-repairs the messages array after a tool-dispatch crash (orphaned tool_calls / tool messages), preventing 400 INVALID_REQUEST session lock-ups.

Install

# from npm (prebuilt)

dsh plugin --profile web add dsh-messages-sanitizer

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:Leeminjing/dsh-messages-sanitizer

Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).

README

🔧 Did your conversation break while creating / loading a plugin in DeepSeek Harness? This plugin fixes exactly that.

While developing or loading a local plugin, one tool-dispatch crash (Cannot read properties of undefined (reading 'prepare')) leaves an orphaned tool_calls in the session, after which every subsequent turn is rejected with 400 INVALID_REQUEST, retries do nothing, and the session is stuck. This plugin automatically repairs the messages array back to a valid state so the conversation continues instead of freezing.

💥 Before                                ✅ After (with this plugin installed)
plugin crashes                           plugin crashes
   ↓                                        ↓
orphan tool_calls                        messages auto-repaired
   ↓                                        ↓
400 INVALID_REQUEST forever              conversation continues
   ↓
conversation dead

A DeepSeek Harness plugin that automatically corrects the messages array — preventing every chat crash caused by an invalid messages array.

Background: the crash you hit

The OpenAI-compatible protocol requires tool calls to come in pairs, and a tool message must immediately follow the assistant tool_calls message (no user / assistant message may be inserted in between):

assistant  { content: ..., tool_calls: [{ id: "call_A", ... }] }
tool       { tool_call_id: "call_A", ... }   ← must immediately follow, covering every id

When a tool dispatch crashes after "the assistant tool_calls / tool/call was recorded but before a tool result was produced" (e.g. ctx.tools[symbol].prepare throws Cannot read properties of undefined), the session log is left with an orphaned tool_calls that has no tool-message response. The next request assembles the history as:

[..., assistant{tool_calls:[write]}, user{...}]        ← invalid

The API answers 400 INVALID_REQUEST, and because the history is unchanged on retry, it is rejected again and again — the session is stuck. If several failed retries follow the crash, the log also ends up with multiple duplicate user messages sitting between the orphaned assistant and the injection point, which makes "inserting a tool message" unable to satisfy the adjacency constraint either.

Background (against the upstream DSH discussion)

The same class of failure is tracked upstream in DeepSeek Harness: Discussion #4843 "Incomplete or isolated tool_calls records cause the DeepSeek API to return 400", which describes "when the session history contains tool_calls with no paired result, or with incomplete id/name/arguments, the chat-completions API returns 400", and ships root-cause patches at the harness level (agent-loop stripping, llm-deepseek synthesizing a fallback id, compaction's tool-pairing keyed by callId rather than count).

This plugin is complementary to, not a replacement for, that fix: it patches DSH at the source, whereas this plugin is a runtime safety net that never modifies the DSH source — it auto-corrects the messages array so already-poisoned sessions, or sessions running on a harness build that still carries this bug / a tool-dispatch crash, are repaired and continue without waiting for the next harness release.

How the plugin fixes it (four layers of defense)

  1. Auto-resume (agent/status, primary path): after a tool-dispatch crash (e.g. prepare throws), once the agent returns to idle it sends a synthetic error tool-result back to the inbox and wakes the agent — reporting the error to the LLM verbatim so the LLM itself decides whether to switch tools, retry, or tell the user. This guarantees the AI message is always last and the conversation never hangs.

  2. Prevention (agent/pre-step): tracks, per session, calls that were "declared but never answered by a tool/result"; before the next request is built it prepends a synthetic error tool-result message to that step's messages (covers stale orphans restored after a restart). The synthetic message is persisted as a user/message event along with decision.messages, so deriveMessages() is valid from the root, eliminating the 400 at the source.

  3. Healing (agent/request-error): if the API still returns 400 for a tool_calls pairing/adjacency violation (e.g. an already-poisoned session from an older version, or stale messages already sitting after the orphaned assistant), it repairs the log with surface replacements, then forces one retry (the retry rebuilds the request from the repaired log and succeeds in one shot):

    • rewrites the dangling assistant message into a version without tool_calls (strips the unanswered calls);
    • neutralizes orphan tool messages (a tool-result with no preceding tool_calls) into plain-text user messages;
    • restores assistants that were wrongly stripped but whose results are still adjacent (re-adds their tool_calls, preserving the historical tool context);
    • folds the duplicate user messages left behind by crash retries. The repair is idempotent: on a second encounter of the same violation there is nothing left to do, so it falls back to the downstream policy — no infinite retry.
  4. Last resort (llm/stream): runs a pure array correction on every request (pairing + adjacency reordering + orphan/duplicate dropping + empty-assistant dropping). Loop-built requests are frozen, so it only warns without rewriting; non-frozen requests that build their own messages (compaction, session-title, …) are replaced in place.

Installation

dsh plugin --profile web add github:Leeminjing/dsh-messages-sanitizer

Restart the harness — the plugin loads automatically as a profile layer.

Update to the latest version:

dsh plugin --profile web update dsh-messages-sanitizer

Configuration

Key Type Default Meaning
enabled boolean true Master switch

To disable, remove the insert entry from cordis.patch.yml, or use:

- insert:
    - id: messages-sanitizer
      name: 'dsh-messages-sanitizer'
      disabled: true

Verification

cd dsh-messages-sanitizer
node --test        # 40 test cases: pure-function correction + session tracking + request-failure healing + real cordis/Session integration

Coverage (all validated against the real @deepseek-ai/dsh-session foldSurface / Session):

  • real crash sequence end-to-end: assistant/message{tool_calls} → tool/call → crash → step/end → turn/end error → after the next-turn injection the wire is valid;
  • a real poisoned log (orphan + stale duplicate user messages) becomes pure user/assistant after repair, with no tool messages left, and the repair is idempotent;
  • surface replacements are executed on a real Session (validated by the Session itself);
  • request-failure healing only intervenes on a tool_calls-pairing 400, forces one retry after repairing, and never retries infinitely.

Directory structure

dsh-messages-sanitizer/
├── package.json      # declares dsh.bundle (the `dsh plugin add` entry point)
├── cordis.patch.yml  # bundle patch layer (mounts messages-sanitizer)
├── LICENSE
├── README.md
├── README.en.md
├── lib/
│   ├── index.js      # plugin entry (name / inject / Config / apply)
│   ├── sanitize.js   # pure messages-array corrector (pairing/adjacency/orphan/duplicate/empty)
│   └── repair.js     # orphan tracking + pre-step prevention + surface-replacement healing + request-failure retry
└── tests/
    ├── sanitize.test.mjs            # pure-function correction cases
    ├── repair.test.mjs              # tracker + pre-step repair + request-failure healing (fake ctx)
    ├── integration.test.mjs         # end-to-end simulation of the real crash sequence
    ├── heal.test.mjs                # healing: normal turns untouched / orphan neutralization / wrong-strip restoration / mixed
    └── cordis-integration.test.mjs  # real cordis + real Session integration

Notes

  • The plugin is zero-build pure ESM, directly loadable by the cordis loader; its runtime dependencies — @deepseek-ai/dsh-llm (synthetic messages), @deepseek-ai/dsh-session (surface folding), @deepseek-ai/cordis, @deepseek-ai/schemastery (config schema) — come from the harness runtime itself.
  • An already-crashed session is auto-healed by the healing path when you keep chatting after a restart (the first request fails once, the dangling calls are stripped, and the retry succeeds).
  • The node_modules in this directory is a junction pointing at the harness runtime's ~/.dsh/profiles/node_modules, used only to resolve dependencies for local node --test; the harness runtime does not depend on it.
  • No rebuild is needed after editing the plugin code; restart the harness (or let cordis HMR reload it) for changes to take effect.

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →

Community comments

Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.