dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 插件:任务结束或命令卡死时用本机人声提醒,自动中断卡住的 pwsh 调用并把「怎么继续」写回工具结果,并在 spawn 之前拦掉已知的性能陷阱写法。仅 Windows。 / Beeps + stuck-command watchdog + command guard for DeepSeek Harness. Windows only.
中文 · English
Gets you out when a command hangs; gets your attention when a task finishes.
A DeepSeek Harness (DSH) plugin that does three things:
pwsh call is confirmed stuck, the plugin aborts that one tool call and writes "what happened + what state the shell is in + how to continue" back into the tool result, so the model can finish the job in the same turn.spawn (returning in ~0 ms with the fix), instead of costing minutes.Sound is played by the harness process itself (System.Media.SoundPlayer / WPF MediaPlayer), not through the browser.
⚠️ Windows only. All three capabilities depend on Windows: the process probe uses
Get-CimInstance Win32_Process, audio goes through .NET / WPF players, and the guard targets PowerShell idioms.package.jsondeclaresos: ["win32"], so it will not install on macOS / Linux — deliberately, which beats installing and hearing nothing. See Platform support.
Once long tasks are running, three things keep biting:
| What happens to you | What it does |
|---|---|
| A long task finishes while you are in another tab and you never notice | Plays a sound on this machine; does not depend on the page being focused |
| A command hangs (waiting for input, deadlock, wedged network) and the turn just sits there | Confirms "no progress", aborts that one call, and writes recovery guidance back to the model |
| The model keeps writing the most expensive idioms (recursive enumeration and friends), costing minutes each time | Refuses it before spawn, returning in 0 s with the replacement |
| Event | When it rings | Default sound |
|---|---|---|
taskEnd | A turn finished | voice:taskEnd |
toolWarn | A command has run long enough to enter the warning window | voice:toolWarn |
toolTimeout | A stuck call was confirmed and aborted | voice:toolTimeout |
recovered | The resume note was written into the tool result and the task continues | voice:recovered |
stall | A whole turn produced no progress | voice:stall |
question | The model asks you something via ask_user_question (client half listens on user-questions/request) | voice:question |
approval | A tool call needs your approval (listens on approval/request) | voice:approval |
The voice prompts are static assets shipped with the package (assets/voice/*.mp3, ~180 KB), synthesised at build time and never at runtime: replace a file with your own to change the voice, or point a settings row at any .wav / .mp3 absolute path. Every row has a ▶ preview button that plays exactly what you will hear.
Neither pwsh form produces session events while it runs (a one-shot call only has tool/start → tool/result; a persistent shell's PTY lives in a realm the host plane cannot read). So the signal comes from the OS instead: enumerate the harness's descendant processes and sum their CPU time and I/O bytes, then watch whether those totals still grow.
Two conditions are both required:
pwsh that is merely alive burns roughly 23 ms of CPU and moves ~6 KB of I/O per second (including its conhost); a process actually working is one to two orders of magnitude above that. So "progress" means >10% of one core or >64 B/ms.Hence: compiling, downloading or writing files → above the floor → hands off; waiting for input (git commit without -m, Read-Host, pause), deadlock or a wedged network wait → only noise → confirmed stuck.
Note that the earliest moment it may act is max(killMs, warnMs + preWarnMs) — lowering killMs alone does nothing:
| Gate | Default | Meaning |
|---|---|---|
warnMs | 120s | Still not returned after this long → ring + page banner (warning window opens) |
preWarnMs | 60s | Time to stop it manually after the ring |
killMs | 180s | Earliest moment it may act, counted from the call's start |
silenceMs | 60s | The watched call's own subtree must stay below the noise floor this long to count as "confirmed no progress" |
Plus three layers of protection: commands matching protectList (npm install / pnpm build / git clone — slow but healthy and quiet) are always allowed to ring but never auto-interrupted; observeOnly mode rings and logs without acting; maxAutoActionsPerSession (default 3) prevents a "kill → resume → run the same thing → kill again" loop. And if the probe cannot read data, nothing is ever interrupted.
taskkill): the tool's own cancellation path runs and a persistent shell resets itself. At the same time a resume note is attached to that tool result, so the model knows within the same turn: the original command, how long it ran, why it was interrupted, that partial output is unavailable, whether the shell was reset (cd / variables lost, working directory back to the workspace), how to proceed (non-interactive / run_in_background / polling a job), and not to re-run it verbatim.escalateGraceMs), the framework layer is wedged: the turn is cancelled and a followup() message wakes the session to continue.Both levels above assume "no tool running = the turn is dead". But ask_user_question is designed to park there and wait for a person — no tool is running, so the user's thinking gets mistaken for a hang.
There are two layers by design, independent of each other:
humanWaitTools (default [ask_user_question]): while such a tool is in flight, the whole-turn judgement is fully suspended (not even a ring), and the silence clock restarts when the answer arrives. It must not go into watchTools: there it would be treated as "a slow tool to watch", and killMs would kill it, which is worse.stallAutoResume: false (the default): the whole-turn layer only rings, and never cancels the turn in any situation, covering waiting shapes outside the question window that nobody anticipated.⚠ Known limitation (layer 1): in a real run,
watchdog.humanWaitswas observed to be empty for the whole duration of a question — layer 1 never saw theask_user_questioncall, so whether it works is unconfirmed. The unit tests exercise a direct call intowatch(), which is why a fully green suite does not catch this. The trail ends at the host's tool-dispatch layer; the root cause is not located.So the layer that actually protects you is layer 2:
stallAutoResume: falseguarantees a turn is never cancelled while you are thinking. The residual symptom is an occasional spurious ring (thestallsound); if it bothers you, raisestallTimeoutMs(e.g. 300000).
To restore whole-turn auto-cancel, set stallAutoResume back to true (if layer 1 does work, the question window stays exempt). Note this governs only the whole-turn layer — the tool layer (warnMs / killMs cutting off a stuck pwsh) is unaffected and remains fully automatic, which is where the plugin's main value lives.
When there are no events, no tools running and no process movement (stallTimeoutMs, default 180s), it only rings. It does not cancel the turn by default; see the previous section for why.
The watchdog treats "hangs"; the guard treats "idioms". A foreground command that hits a rule is never spawned: the guard answers with a tool error carrying the fix in ~0 ms. run_in_background: true is the escape hatch for when the slow form is genuinely wanted (background work cannot stall a turn, so the guard lets it through).
Same directory, same files, different idioms, measured:
| Idiom | Time |
|---|---|
Get-ChildItem -Recurse -Include *.js | >90s |
Get-ChildItem -Recurse -Filter *.js | 6.7s |
the native grep tool | 0.23s |
The rules, each naming the real risk it prevents:
| Rule | What it refuses | The fix |
|---|---|---|
tilde-native-path | node ~/x, git -C ~/x, pwsh -File ~/x — ~ is only expanded for a cmdlet's Path parameter, so handing it to a native command passes a literal and must fail | Use an absolute path (no "run it in the background" escape: backgrounding a broken path still fails) |
recurse-include | -Recurse + -Include | -Filter, or the native grep / glob tools |
select-last-unbounded-process | Select-Object -Last piped from a test runner | Redirect to a file and read its tail, or go background |
foreground-long-sleep | foreground Start-Sleep -Seconds >= 60 | A polling script with a timeout, or go background |
explicit-long-timeout | an explicit timeoutMs >= 180000 on a command outside the protect list | Start it with run_in_background |
The rules were measured, not guessed. The first cut also refused -Recurse + Format-Table / + Select-String / + node_modules; replaying the whole foreground command history showed they average only 4.3–11.7 s, while a refusal forces a rewrite round trip — the rewrite costs more than running it. Those three produced 87% of the refusals for 12% of the benefit, so they were removed; their rare long tail (117–121 s) is what hardCeilingMs is for. Any new rule has to be measured the same way before it ships.
Config: guardEnabled (default true), guardAllow (substrings that exempt a command, default empty).
Recommended companion setting: hardCeilingMs: 120000 — a hard two-minute ceiling for non-protected commands, specifically to counter "the model keeps raising timeoutMs". Protected commands are exempt from the ceiling.
# From GitHub (pin to a commit if you like)
dsh plugin --profile web add github:kiterunner1/dsh-attention-beep
# Or clone first and install locally (source edits take effect immediately)
git clone https://github.com/kiterunner1/dsh-attention-beep.git
cd dsh-attention-beep
dsh plugin --profile web add .
Stop the running DSH before installing plugins, otherwise profiles/<name>/node_modules gets deleted half-way and you are left with a profile that runs now and dies on restart.
Editing host code (lib/*.js) requires restarting DSH (ESM module cache); the client half (lib/client.js) only needs a page refresh.
Uninstall: dsh plugin --profile web remove dsh-attention-beep, and drop the matching entry from dsh.profile.bundles.
Everything lives in the loader row's config (defaults in cordis.patch.yml). The user layer overrides by id, or you edit it in Settings → Beeps (written to $DSH_HOME/settings.yaml, effective live):
- id: attention-beep
config:
enabled: true
scope: root # beeps: root | all (include sub-agents)
watchScope: all # watchdog: root | all (watch sub-agents too)
watchTools: [pwsh]
humanWaitTools: [ask_user_question] # suspends the whole-turn judgement (do NOT move into watchTools)
warnMs: 120000
killMs: 180000
preWarnMs: 60000
silenceMs: 60000
sampleIntervalMs: 10000
hardCeilingMs: 120000 # 0 = off; >0 interrupts even while CPU is burning (loop protection)
autoKill: true
observeOnly: false # dry run: ring / log / notify only
escalateToTurnCancel: true
escalateGraceMs: 45000
maxAutoActionsPerSession: 3
stallTimeoutMs: 180000
stallAutoResume: false # whole-turn layer only rings, never cancels (keeps your thinking safe)
guardEnabled: true
guardAllow: [] # substrings that skip the guard
protectList: [npm install, pnpm build, git clone, ...] # omit = built-in list
logPath: '' # empty = <DSH_HOME>/logs/attention-beep.log; "" disables
events:
toolTimeout: { enabled: true, sound: voice:toolTimeout }
sound accepts three forms: voice:<key> (bundled voice), a preset name (ding / chime / notify / tada / alarm / error / default / recycle / ring / beep, all %WINDIR%\Media\*.wav), or any absolute .wav / .mp3 path. A missing file falls back to beep.
Event log: $DSH_HOME/logs/attention-beep.log, a full JSONL stream (warn / action / settled-aborted / resume / sound / breaker / stall / guard), rotated past 2 MB.
Windows only. Capability by capability:
| Capability | On non-Windows |
|---|---|
| Beeps | Depend on System.Media.SoundPlayer / WPF MediaPlayer; no equivalent implementation |
| Watchdog | The probe uses Get-CimInstance Win32_Process; elsewhere probe.supported = false and auto-abort does nothing (no crash, just no action) |
| Command guard | The rules target PowerShell syntax; DSH ships the bash tool on those platforms, and the default watchTools: [pwsh] never matches |
So package.json declares os: ["win32"]: it will not install elsewhere. That is deliberate — better than installing and hearing nothing.
The one guard rule that could misfire on POSIX (tilde-native-path) is additionally gated on the platform, because there the shell expands ~ itself and node ~/x is perfectly valid.
node --test test/unit/*.test.mjs # 82 tests: decision state machine, probe, audio resolution, notice routes, guard rules, report copy
node test/e2e/run.mjs all # real DSH: one-shot pwsh / persistent pwsh / regression
The E2E run starts an isolated headless profile, launches a 600-second silent command, then verifies: the ring was recorded, the call was auto-aborted, the model received the resume note inside the tool result, the call took far less than 600 s, and no processes were left behind.
sampleIntervalMs, and only while a watched call is running; it backs off to three times the interval while progress continues and resumes immediately when work stops.Q: Will it kill a legitimately long task?
The signal is "the call's own subtree stopped accruing CPU and I/O", it must clear the idle noise floor, and it must stay silent for silenceMs. Compiling, downloading and writing files all keep producing CPU/I/O and are never mistaken for hangs. protectList additionally disables auto-interruption entirely for commands that are known to be slow but quiet (npm install and friends).
Q: Why is stallAutoResume false by default?
The whole-turn assumption "no events = dead" does not hold: a model thinking slowly and a user typing an answer both look like "no events". The whole-turn layer only rings and leaves the decision to you. The layer that is genuinely valuable — and still fully automatic — is the tool layer, cutting off the one stuck command.
Q: Can it ring without ever interrupting?
Yes: observeOnly: true for a dry run (ring / log / notify), or autoKill: false.
Q: How do I change the voice or mute an event?
Point that event's sound at your own .wav / .mp3 in the settings page, or set its enabled to false. Every row has a ▶ preview button.
Q: Does it slow DSH down? The probe is a short-lived PowerShell child, once every 10 seconds, and only while a watched call is running; it backs off to 3× the interval while progress continues. When idle it does not sample at all.
MIT — see LICENSE.