Tools & Capabilities

dsh-midtalk

blueberrymaid/dsh-midtalk · ★ 1

blueberrymaid

任务内插话与可选打断:/say 无损递话(插到下一个步骤边界),/cut 中止当前步并留下结构化恢复卡

License
MIT
Added
2026-10-30
Maintenance
active
Install kind
github
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-10-30T09:39:05.305Z
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

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

Source README · body: en · Read Chinese README

Table of contents
  1. 1. The problem
  2. 2. Commands
  3. 3. Install
  4. Option A: dsh plugin add (recommended, restart required)
  5. Option B: place it in a profile + roster (no pnpm / no network)
  6. How to verify it is loaded
  7. Appendix: no-restart path (dsh-my-guardian staging, as tested locally)
  8. Uninstall / rollback
  9. 4. Recovery card
  10. 5. Hard rule line (plugin-only)
  11. 6. What it deliberately does not do
  12. 7. Undocumented internals it depends on
  13. 8. Known limits
  14. 9. Development
  15. 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 — only index.js uses it to build the injected message; lib/core.js and 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.json sets "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.

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 stops dsh web from 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 }
  • version must match what you installed; stage reaches registered only after the commands are registered.
  • pid is the process that wrote the file. If the version does not change, or stage stays at apply, 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 /say and /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:

  1. copy the package to $DSH_HOME/profiles/web/node_modules/dsh-midtalk/;
  2. write one line into $DSH_HOME/profiles/web/cordis.staged.json:
    [{"id":"midtalk","name":"./node_modules/dsh-midtalk/index.js?v=1"}]
    
  3. within seconds guardian tries to mount it: on success the staging file is emptied and the entry appears in promoted inside $DSH_HOME/guardian/state.json; on failure the line stays and lastError explains why (3 consecutive failures freeze it).

Two traps we hit for real:

  • A new module name needs a new id. Keeping the old id with a new name is 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-report stayed inert across three installs). The only lever is a query string on the module name (?v=2) — then confirm with version in boot.json.

Uninstall / rollback

  1. remove that insert: entry from cordis.patch.yml (with guardian you can instead POST http://127.0.0.1:3080/guardian/api/remove with {"id":"<staged id>"} for a hot unmount);
  2. delete $DSH_HOME/profiles/<profile>/node_modules/dsh-midtalk/;
  3. 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):

  1. list state first, do not continue working;
  2. read the card — especially pendingToolCalls, the tool calls that were in flight at the moment of interruption (name + arguments);
  3. 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);
  4. hand that assessment (landed / roll back / verify first) to the user for a decision;
  5. 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.disarm are 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 /say into /cut. That would turn your one guarantee ("/say never 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

  • /say latency = 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 /cut at a time) cannot reach it; only rapid repeated /cut can.
  • 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 / lastReasoning are clipped at 2000 chars (lib/core.js:24-25); frames keeps 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).