Sends a desktop notification and chime when a DeepSeek Harness run completes, errors, or waits for approval, with a configurable duration threshold.
Install
# from npm (prebuilt)
dsh plugin --profile web add task-complete-notify-for-dsh
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:lxp731/agents-plugins#path:/task-complete-notify-for-dsh
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
DSH (DeepSeek Harness) plugin: desktop notification + chime when a run finishes — completion, errors, and approval requests — with a configurable duration threshold.
The essence: a custom chime that works on all three OSes. Replace
lib/assets/prompt-tone.mp3with any audio you like and the sound changes everywhere — Windows, macOS, Linux — not just Linux.
Cross-platform
Runs on Windows, macOS and Linux from the same package — no code changes. Notification and the custom chime work on each OS (see Platform support).
Features
| Feature | Details |
|---|---|
| Run completion | ✅ notify + chime |
Run error (turn/end reason=error) |
✅ critical "任务失败" |
Approval request (approval/asked) |
✅ critical "等待审批" |
Model question (ask_user_question) |
✅ critical "模型需要你回答" |
| Duration threshold | ✅ configurable |
Model-callable notify tool |
✅ (rate-limited) |
notify-threshold command |
✅ persists to user config |
| Cross-platform | ✅ Windows / macOS / Linux |
| Custom chime | ✅ chimeFile config or replace the mp3 |
| i18n messages | ✅ lang: 'zh' (default) or 'en' |
| Anti-spam cooldowns | ✅ for approval retries and tool calls |
| Failure diagnostics | ✅ every notification/persistence failure is logged |
Why this is a safer install than curl | bash plugins
This package ships pre-built lib/ with no prepare/postinstall/install build script — dsh plugin add has nothing to execute beyond loading the published JS. Build-time code only runs on the author's machine at publish (prepublishOnly), never on your machine at install. This avoids the "install runs a remote build script" supply-chain concern.
Install
Source: github.com/lxp731/agents-plugins/tree/main/task-complete-notify-for-dsh
From npm (recommended):
dsh plugin --profile web add task-complete-notify-for-dsh
From the GitHub monorepo (alternative): the plugin lives in the
agents-plugins monorepo, so point dsh at the subdirectory with #path::
dsh plugin --profile web add github:lxp731/agents-plugins#path:/task-complete-notify-for-dsh
Then restart dsh web. Or via the dsh-market panel: Settings → Plugin Market.
How it works
- One notification per run, not per turn. A run can span many turns; the plugin records the latest
turn/endreason and fires once when the root agent returns toagent/status'idle'(the harness's own "run ended" signal). - Errors and token limits carry the final
turn/endreason, so a run that errors gets a critical "任务失败" with the true final result. - Approval / question fire an immediate critical notification, because each is a separate "the session is waiting on you" moment.
- Subagent sessions are excluded (
header.origin === 'subagent') so a top-level run produces one notification.
Configuration
Declare the row in ~/.dsh/profiles/web/cordis.patch.yml (the user layer, applied last):
- id: task-complete-notify
name: task-complete-notify-for-dsh
config:
enabled: true # master switch
threshold: 0 # min run duration (s) before notifying; 0 = always
title: DeepSeek Harness # notification title
sound: true # play a chime
onQuestion: true # notify when the model asks a question
onApproval: true # notify when the harness waits for approval
chimeFile: '' # absolute path to a custom mp3; empty = bundled default
lang: zh # message language: 'zh' (default) or 'en'
blockedCooldownSec: 60 # min seconds between question/approval notifications per session
toolCooldownSec: 10 # min seconds between model-invoked notify tool calls
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Master switch; false disables everything |
threshold |
number | 0 |
Only notify when the run took longer than this (seconds). 0 = always notify |
title |
string | DeepSeek Harness |
Notification title |
sound |
boolean | true |
Play a chime with the notification |
onQuestion |
boolean | true |
Notify when the model calls ask_user_question |
onApproval |
boolean | true |
Notify when the harness waits for approval |
chimeFile |
string | '' |
Absolute path to a custom chime mp3, e.g. /usr/local/music/xx.mp3. Overrides the bundled prompt-tone.mp3. Empty uses the default |
lang |
string | 'zh' |
Message language for notifications and command replies ('zh' / 'en') |
blockedCooldownSec |
number | 60 |
Minimum seconds between blocking-event notifications (question/approval) for the same session — retries no longer spam while you're away |
toolCooldownSec |
number | 10 |
Minimum seconds between model-invoked notify tool calls; faster calls return { ok: false, throttled: true } |
Runtime threshold (/notify-threshold) — persists
/notify-threshold <秒> sets the threshold and persists it to the user layer
(cordis.patch.yml), so it survives a restart — no need to edit the file by
hand. With no argument it prints the current value. Example:
/notify-threshold 30 notifies only on runs longer than 30s.
Custom chime (chimeFile)
Point chimeFile at your own audio file to override the bundled prompt:
- id: task-complete-notify
name: task-complete-notify-for-dsh
config:
chimeFile: /usr/local/music/xx.mp3
Any mp3 works; the file is played on all platforms (see Platform support). If
chimeFile is configured but the file is missing, the plugin logs a warning at
startup and falls back to the default.
Model-callable notify tool
The model can explicitly notify the user for important events:
notify(message: "Data export complete: 100,000 records", status: "success")
notify(message: "Build failed: TypeScript compilation error", status: "failure")
| Parameter | Type | Description |
|---|---|---|
message |
string | Notification content (required) |
status |
"success" / "failure" |
Optional; "failure" uses critical urgency. Defaults to "success" |
Breaking change in v0.2.0:
statusvalues changed from"完成"/"失败" to"success"/"failure"`. Update any prompts that pass Chinese values.
Calls are rate-limited (toolCooldownSec, default 10s); suppressed calls return { ok: false, throttled: true }.
Platform support
| Platform | Notification | Chime (custom mp3) |
|---|---|---|
| Linux | notify-send |
mpv → ffplay → pw-play → cvlc → paplay |
| macOS | osascript display notification |
afplay (built-in) → mpv → ffplay → cvlc → mpg123 |
| Windows | WinRT Toast (no focus stealing; Popup fallback) | mpv → ffplay → mplayer → mpg123 → cvlc |
On Linux, the display session (DISPLAY/WAYLAND_DISPLAY) is discovered from
the actual session sockets instead of hardcoded defaults, so notifications work
under systemd user services too.
The custom chime works on every platform. The plugin's core idea is a
replaceable audio file (lib/assets/prompt-tone.mp3) — swap the file to change
the sound. On all three OSes it first auto-detects an available player and
plays the mp3; only when no player exists does it fall back to the platform
system sound (macOS Glass / Windows SystemSounds).
- macOS:
afplayships with the OS and plays mp3 natively, so the custom chime works with zero install. - Windows: has no built-in mp3 player (PowerShell's
SoundPlayeronly plays.wav), so install any one of mpv / ffplay / mplayer / mpg123 / cvlc to enable the custom chime.
To change the sound without touching config, replace lib/assets/prompt-tone.mp3
(any mp3 works). To keep your own file elsewhere, use the chimeFile config.
Notifications are fire-and-forget (detached, unref'd) — a missing notifier never breaks the run it reports on. Every failure is logged via the plugin logger, so a silent no-op is diagnosable from dsh logs.
Reliability
- Self-check: warns once if no harness events arrive within 5 minutes of an active session — an early signal that a dsh update changed its event API.
- Profile safety: if the active profile cannot be determined, persistence falls back to
'web'and logs a warning. - Config persistence preserves comments: edits go through a YAML AST, so your annotations in
cordis.patch.ymlsurvive/notify-threshold; writes are atomic (temp file + rename) with a best-effort lockfile. - Bounded memory: per-session tracking state is capped and consumed at idle.
See CHANGELOG.md for the full history.
Development
npm install # resolves schemastery + peers via package-lock.json
npm test # node --test tests/*.test.mjs
npm run lint # eslint lib tests
If peer deps aren't resolvable from a fresh clone, bash scripts/smoke.sh links them from a dsh install's node_modules automatically.
Install locally for testing (from a clone of the agents-plugins monorepo):
git clone git@github.com:lxp731/agents-plugins.git
cd agents-plugins/task-complete-notify-for-dsh
dsh plugin --profile web add link:.
Note for systemd-managed setups (
dsh-service-control): after any change tolib/, reload the plugin by restarting the service, e.g.systemctl --user restart dsh-web.service.
Development notes (read before extending)
- Cordis services must be injected.
ctx.toolsandctx.commandsare not directly readable — accessing them without declaration throwscannot get property "tools" without injectand fails the whole plugin tree at boot. Always usectx.inject(['tools'], (sctx) => …)/ctx.inject(['commands'], …)and register insidesctx.effect(...). - The harness's typed
session/event/agent/statusdeclarations live in the unpublished@deepseek-ai/dsh-session/dsh-agentpackages, so event listeners register with a narrowed cast; payload shapes are pinned inlib/notifier.js. - Config persistence uses the
yamlpackage to read/merge/write the plugin's row in the profile user layer (cordis.patch.yml, vialib/persist.js).DSH_HOMEalready points at the.dshdata dir (default~/.dsh); the resolver must NOT append.dshagain whenDSH_HOMEis set. - Profile resolution is robust:
resolveProfile()picks the active profile from, in order, an explicit argument, theDSH_PROFILEenv var,--profilein argv, then a'web'fallback. Never rely on argv alone. writeConfigis crash-safe: it refuses to clobber acordis.patch.ymlthat isn't a valid top-level YAML array, and never overwrites an unparseable file with an empty one (that would wipe a real user config).- The
notifytool is built withdefineTool()from@deepseek-ai/dsh-tools(a harness peer).defineToolrequiresrequired: trueon mandatory parameters; omitrequiredentirely for optional ones (a barerequired: falseis rejected).
License
MIT
Links
More in this category
xmanrui/dsh-im★ 1636
Connect IM bots to DeepSeek Harness via QR codes or bot credentials (9 channels: Feishu, WeChat, DingTalk, WeCom, QQ, Slack, Telegram, Discord, and WhatsApp).
shaobeichen/dsh-pocket★ 1558
Remote phone access to the DSH Web UI: scan a QR code for LAN or public (cloudflared tunnel) access with real-time sync, a mobile-adaptive layout, and a settings tab.
inclusionAI/Avernet#deepseek-harness-channel-bcn★ 677
Connects DeepSeek Harness to Avernet's Bot Collaboration Network over WebSocket V2, with automatic onboarding, isolated agent sessions, tool-call events, and multi-bot routing tools.
omdsh-dev/dsh-notification★ 85
Desktop notifications for turn completions, with per-outcome controls and keyword rules.
whyihaveyou/dsh-suite#plugin-notify★ 56
IM webhook and local notifications on turn completion, errors, or approval (Feishu/WeCom/DingTalk/Slack/Discord/custom).
omdsh-dev/dsh-lark★ 55
Lark/Feishu bot channel for DeepSeek Harness: each chat drives its own agent, and tool approvals, model questions, and plan reviews return as cards answered by a button or a reply. Switch workspace and model from the chat (`/cd`, `/model`, `/new`), and run several bots that keep separate sessions and can hand turns to each other in one group.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.