Skip to content
dsh-market Browse plugins GitHub 中文

Leeminjing/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.

Stars ★ 1 Category Sessions & Messages Listed 2026-08-15

Install

Inside DeepSeek Harness, with dsh-market

dsh plugin --profile web add dshmarket

Or from the command line

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

Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.

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.

How the plugin fixes it (three layers of defense)

  1. Prevention (agent/pre-step, primary path): tracks, per session, calls that were "declared but never answered by a tool/result"; only when the assistant that declared them is the last node of the surface (the typical shape of a fresh crash), it prepends a synthetic error tool-result message to the incoming messages before the model request is built. The synthetic message is persisted as a user/message event along with decision.messages, so deriveMessages() is valid from the root — the loop-built request (deep-frozen and immutable) is valid too, eliminating the 400 at the source.

  2. 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.
  3. 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.

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/endturn/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 ↗