Tools & Capabilities
dsh-midtalk
blueberrymaid/dsh-midtalk · ★ 1
任务内插话与可选打断:/say 无损递话(插到下一个步骤边界),/cut 中止当前步并留下结构化恢复卡
Manifest details
安装
dsh plugin --profile web add 'github:blueberrymaid/dsh-midtalk#feee0b91840e804b7104f6743673e40be85a3aee'GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
- 1. The problem
- 2. Commands
- 3. Install
- Option A: dsh plugin add (recommended, restart required)
- Option B: place it in a profile + roster (no pnpm / no network)
- How to verify it is loaded
- Appendix: no-restart path (dsh-my-guardian staging, as tested locally)
- Uninstall / rollback
- 4. Recovery card
- 5. Hard rule line (plugin-only)
- 6. What it deliberately does not do
- 7. Undocumented internals it depends on
- 8. Known limits
- 9. Development
- 10. License
Mid-task interjection + optional interruption + a structured recovery card for DeepSeek Harness (DSH). Two slash commands that separate "I want to say something" from "stop this step right now" — without losing the scene when it stops.
English · 中文
- Host-side Cordis plugin only. No client code, no build step, no third-party runtime dependencies; there is one peer dependency on the host package
@deepseek-ai/dsh-llm— onlyindex.jsuses it to build the injected message;lib/core.jsand the self-test do not need it. - No model calls, no network, no telemetry. It only writes its own boot marker, wake log and recovery cards under
$DSH_HOME/midtalk/. - Not distributed via npm:
package.jsonsets"private": true; you install it by placing the directory into a profile (§3).
1. The problem
While a task is running you have two extremes:
| Option | Cost |
|---|---|
| Wait for the turn to finish | Slow — a long step can take minutes |
| Hit the UI stop button | Instant, but it kills the current step, may leave half-written state, and your sentence gets no reply |
This plugin splits those into two commands and lets one Enter key decide. It never upgrades /say into /cut on its own (a deliberate choice — see §6).
2. Commands
/say <text> |
/cut [text] |
|
|---|---|---|
| Semantics | Lossless: insert at the next step boundary | Lossy: abort the current step |
| Interrupts? | No | Yes (in-flight tool calls are cancelled) |
| Latency | = remaining time of the current step | waits for the agent to reach idle, 3 s fallback (one local observation: 27 ms — evidence in $DSH_HOME/midtalk/wake.log) |
| Turn | Answered within the same turn | Current turn ends; your text arrives as a new turn |
| Leftovers | None | Possibly (the aborted step) |
| Recovery | Not needed | A structured recovery card is written automatically |
| Use when | Default | I'm stuck in a very long step and you must stop me now |
Both commands append "the last tool call started X seconds ago (estimate)" to their reply — information only, semantics unchanged.
3. Install
The package declares dsh.bundle.patch in package.json (pointing at cordis.patch.yml in the repo root), so DSH's own plugin command can install it into a profile as a layer; placing it by hand works too (Option B).
Both files are required: index.js and lib/core.js — index.js:44 imports ./lib/core.js relatively, so a missing file means a failed mount.
Option A: dsh plugin add (recommended, restart required)
dsh plugin --profile web add "github:blueberrymaid/dsh-midtalk"
A thin pnpm forwarder (@deepseek-ai/dsh/lib/plugin-Ddi42qoW.js:7-17): it runs pnpm add <spec> inside the profile directory, then reconciles the profile's layer list against the installed state — a dependency that resolves to a package declaring dsh.bundle joins dsh.profile.bundles; anything else is installed as a plain dependency with a one-line warning (:25-33, :46-78). So dsh.bundle.patch is not decoration: without it the plugin installs but never becomes a profile layer. Restart dsh web to activate.
Option B: place it in a profile + roster (no pnpm / no network)
Step 1 — put it into the module directory
# from GitHub
git clone https://github.com/blueberrymaid/dsh-midtalk.git dsh-midtalk
Copy the whole directory (at least index.js + lib/core.js) into:
$DSH_HOME/profiles/<profile>/node_modules/dsh-midtalk/
$DSH_HOME defaults to .dsh under your home directory; on Windows the real path looks like
C:\Users\<you>\.dsh\profiles\web\node_modules\dsh-midtalk\ (the profile name is usually web; check the directories under $DSH_HOME\profiles\).
Step 2 — add it to the roster
Add one insert: entry to $DSH_HOME/profiles/<profile>/cordis.patch.yml:
- insert:
- id: midtalk
name: dsh-midtalk
Back that file up first (e.g.
Copy-Item cordis.patch.yml cordis.patch.yml.bak). It is read once at startup, all-or-nothing: one bad line stopsdsh webfrom booting.
name also accepts a relative or absolute path (e.g. ./node_modules/dsh-midtalk/index.js).
Step 3 — restart dsh web, then verify below.
How to verify it is loaded
Read $DSH_HOME/midtalk/boot.json — the plugin writes it in apply() and again once command registration finishes (index.js:210, index.js:235):
{ "plugin": "dsh-midtalk", "version": "1.0.0", "stage": "registered", "commands": ["say", "cut"], "pid": 18092 }
versionmust match what you installed;stagereachesregisteredonly after the commands are registered.pidis the process that wrote the file. If the version does not change, orstagestays atapply, the live process is not running the code you just copied — the ESM module cache can serve the old version; a restart is the reliable cure.- Typing
/in the composer should list/sayand/cut.
Appendix: no-restart path (dsh-my-guardian staging, as tested locally)
Machines with dsh-my-guardian installed can hot-mount through its staging file, without restarting. This is the flow the author tested locally — not a general requirement:
- copy the package to
$DSH_HOME/profiles/web/node_modules/dsh-midtalk/; - write one line into
$DSH_HOME/profiles/web/cordis.staged.json:[{"id":"midtalk","name":"./node_modules/dsh-midtalk/index.js?v=1"}] - within seconds guardian tries to mount it: on success the staging file is emptied and the entry appears in
promotedinside$DSH_HOME/guardian/state.json; on failure the line stays andlastErrorexplains why (3 consecutive failures freeze it).
Two traps we hit for real:
- A new module name needs a new
id. Keeping the oldidwith a newnameis silently ignored (the staging line just sits there). - Re-mounting the same path does not reload code. The ESM module cache keeps the old code alive (measured:
dsh-progress-reportstayed inert across three installs). The only lever is a query string on the module name (?v=2) — then confirm withversioninboot.json.
Uninstall / rollback
- remove that
insert:entry fromcordis.patch.yml(with guardian you can insteadPOST http://127.0.0.1:3080/guardian/api/removewith{"id":"<staged id>"}for a hot unmount); - delete
$DSH_HOME/profiles/<profile>/node_modules/dsh-midtalk/; - optionally delete
$DSH_HOME/midtalk/(boot marker, wake log, recovery cards — plain text, safe to keep).
4. Recovery card
Before interrupting, /cut writes the scene to $DSH_HOME/midtalk/last-interrupt-<sessionId>.json — one card per session, never overwritten across sessions; written atomically (temp file + same-directory rename, so a half-written JSON is impossible).
Fields: at, reason, plugin, version, agentStatus, sessionId, interruptedAt{turn,step,attemptId}, userText, pendingToolCalls[]{at,attemptId,callId,name,arguments} (arguments parsed to objects), lastReasoning, lastText, usage, lastFinish, diagnostics (frame-type counters, parse errors) and frames (last 12 stream frames).
Recovery protocol (after an interruption; relies on this card only, no other file):
- list state first, do not continue working;
- read the card — especially
pendingToolCalls, the tool calls that were in flight at the moment of interruption (name + arguments); - decide for each one whether it landed: the card records intent, not an inventory, so check the files/commands it touched yourself (read the file, check exit codes, compare timestamps);
- hand that assessment (landed / roll back / verify first) to the user for a decision;
- after their confirmation, redo the interrupted step.
agent.cancel does not undo anything already written.
5. Hard rule line (plugin-only)
Both commands end their injected text with:
〔硬规则 · 收到插话先输出一句正文回复(确认收到 + 回应内容),再决定是否继续调用工具;只发工具调用、或不回话=违规〕
It is hard-coded in lib/core.js:16 and does not go into the host's per-turn AGENTS.md: this is the plugin's own behavioural constraint and should not pollute global instructions. It comes from a real incident: an interjection was answered with tool calls only, twice. It is a text constraint with no enforcement layer — but it makes "asked and not answered" an explicit violation rather than a style question.
6. What it deliberately does not do
- No background killing:
jobs.kill/terminals.kill/subagents.interrupt/goals.disarmare not wired. Killing background work creates half-done state, which contradicts the whole point. - No automatic threshold upgrade: it will not watch how long the current step ran and silently turn
/sayinto/cut. That would turn your one guarantee ("/saynever breaks anything") into a conditional guarantee decided by state you cannot see. - No hiding of the bracketed text: injected behaviour notes render as a system row (
source: {kind:"plugin", form:"notice"}), clearly not your message — but visible. True invisibility needs an unrendered channel; not available. - No rollback of already-written files (the platform has none).
7. Undocumented internals it depends on
Every row below was checked against the sources; DSH version tested: @deepseek-ai/dsh 0.1.5-rc.3 and @deepseek-ai/dsh-llm 0.1.5-rc.3 (read from version in those packages' package.json:4). Upgrading DSH can change any of them — and when one changes the plugin fails silently (it is dynamically loaded JS, so there is no compile error).
| Use | Evidence (package + file:line) |
|---|---|
| Lossless delivery | agent.steer(createUserMessage(...)); createUserMessage comes from @deepseek-ai/dsh-llm (top-level import at index.js:25, used at index.js:177) |
| Lossy delivery | agent.cancel({kind:"user"}, {keepInbox:true}) — same call as the UI stop button: @deepseek-ai/dsh-api-session-controller/lib/index.js:872-878 (called at index.js:198) |
| Wake timing | agent.status getter (anything but idle/maintenance is running): @deepseek-ai/dsh-agent-loop/lib/index.js:773-775; status changes emit agent/status: same file :776-782; the payload carries the agent: @deepseek-ai/dsh-tool-cordis/lib/index.js:5061-5065 |
| Card content | agent/assistant-stream frames (subscribed at index.js:211); frame.chunk is an object (StreamChunk), not a string (lib/core.js:65-70 accepts both) |
| System row | source: {kind:"plugin", plugin, form:"notice", summary} (pattern: @deepseek-ai/dsh-agent/lib/index.js:99-114; used at index.js:148-153) |
| Command registration | ctx.commands.register({name, description, input, handler}): @deepseek-ai/dsh-commands/lib/index.js:257-259; duplicate names throw: same file :82 (and there is no public unregister API — unmount the old plugin before mounting a new version) |
| Command invocation payload | the handler receives {agent, rawInput, ...} (index.js:166-171) |
8. Known limits
/saylatency = remaining time of the current step (a 100-second step means 100 seconds of silence; split long work into short steps or use/cut).- The pending-wake queue caps at 8 and silently drops the oldest:
MAX_PENDING_WAKES = 8(lib/core.js:20). More than 8 queued sends for one agent means the oldest is dropped (index.js:132-143), leaving only a"why":"dropped:queue-full"line in$DSH_HOME/midtalk/wake.log— the user is never told. Normal use (one/cutat a time) cannot reach it; only rapid repeated/cutcan. - The card records tool-call intent, not a "did it land" inventory — that is not structurally available.
- More than 20 tool calls in one step: only the last 20 are kept (
lib/core.js:26). lastText/lastReasoningare clipped at 2000 chars (lib/core.js:24-25);frameskeeps only the last 12 (lib/core.js:21).- An empty
/say(no text) injects an interjection with no content. - Compatibility was verified only against the version tested (0.1.5-rc.3): 2026-09-25, Windows + node 22. Linux / macOS were not tested (no platform-specific branches in the code, but it has never been run there).
9. Development
dsh-midtalk/
├── index.js host half: command registration, cancel, disk writes, event subscriptions
├── lib/core.js pure logic: frames→trace, trace→recovery card, injected text (zero deps; the self-test covers only this layer)
├── test/plugin.spec.mjs self-test (no framework, node built-ins only)
├── .github/workflows/test.yml CI: ubuntu-latest + windows-latest × node 22
├── .gitignore / .gitattributes
├── cordis.patch.yml layer patch referenced by dsh.bundle.patch (used by Option A)
├── package.json
├── README.md / README.en.md
└── CHANGELOG.md / LICENSE
node test/plugin.spec.mjs # dsh-midtalk self-test: 38 passed, 0 failed
The self-test covers lib/core.js only (it imports no DSH package, touches no filesystem and writes nothing under .dsh), so it runs anywhere, on any platform, with no npm install.
10. License
MIT (see LICENSE).