JohnXu22786
notifier
dsh-chime: desktop signal plugin for DeepSeek Harness — desktop notifications and tones when a task finishes, waits for approval, or errors out
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 16, 2026
- Updated
- Aug 16, 2026
Introduction
dsh-chime
A desktop alert signal plugin running on dsh (DeepSeek Harness): it pops a system notification on the machine running dsh when a task finishes, approval is pending, or a run errors out — and optionally plays a tone. No need to keep watching the terminal or the web UI.
Zero runtime dependencies: no npm notification libraries required, it calls the OS's built-in capabilities directly (osascript/afplay on macOS, notify-send/audio backends on Linux, native PowerShell components on Windows).
Table of Contents
- Design Philosophy
- Features
- Quick Start
- Installing in DSH
- Integration Notes (dsh loading mechanism)
- Event-to-Signal Mapping
- Configuration
- Command-Line Tool
- Platforms and System Dependencies
- Troubleshooting
- Development
- License
Design Philosophy
"Remind only when a human needs to come back — never interrupt for every micro-event" is the plugin's core principle. dsh emits a flood of events on every generated response and every tool call; if all of them popped notifications, reminders would become noise. So dsh-chime distills just three kinds of signals:
| Signal | Meaning | Default tone |
|---|---|---|
done | A turn completed fully (generation finished) | completion tone |
blocked | Waiting for a real human to approve / needing human intervention | alert tone |
failed | A turn or request errored | error tone |
Signals flow internally through a four-stage pipeline, and every stage can be configured or disabled independently:
Capture (bridge listens to dsh events)
→ Adjudicate (policy: master switch → per-kind switch → hush window → cadence limit)
→ Render (templates: title/body templates)
→ Deliver (channels: desktop notification / tone / terminal bell)
Features
- Independent control of the three signal kinds: done / approval / error can each be toggled, with custom wording and sounds (event-type filtering).
- Cross-platform desktop notifications: native implementations on macOS, Windows and Linux, no third-party packages.
- Customizable tones: system sounds or custom audio files, with volume support; can also be fully muted.
- Custom message templates: placeholders supported in both title and body (session, project, elapsed time, error summary, etc.).
- Custom icon: the desktop notification icon can point to an image file (
.icorequired on Windows, any format on Linux). - Hush window: supports windows crossing midnight (e.g. 22:00–08:00) and per-weekday activation.
- Cadence limiting: per-kind minimum interval + global burst cap to prevent notification flooding.
- Approval grace period: approval requests auto-adjudicated (policy rejection / no responder) never disturb; only real human-waiting approvals alert.
- Per-session elapsed time: the done notification includes this turn's elapsed time.
- Command-line self-check: ring a test tone, probe channel availability and view the effective config without starting dsh.
Quick Start
Prerequisites: Node.js ≥ 22.19 (dsh's runtime requirement), dsh CLI installed and a profile initialized.
# Run from a directory containing this package; link: points to the local directory
dsh plugin --profile web add link:/absolute/path/notifier
Or after publishing to npm:
dsh plugin --profile web add dsh-chime
Verify the integration:
# A "# == dsh-chime layer" entry should appear in the config tree
dsh --profile web --dump-config
# Start dsh and watch for [chime] lines in the log
dsh --profile web
Test a ring (no need for dsh to be running):
chime probe # check availability of each channel
chime ping done # pop a "task finished" test notification + tone
Note: notifications appear on the machine where the dsh service process runs. If dsh runs on a remote server, the reminders appear on the server's desktop, not on yours.
Installing in DSH
Install directly from the GitHub repository with the dsh plugin command:
dsh plugin --profile demo add github:JohnXu22786/notifier
After installation, the plugin registers itself as a configuration layer (dsh.bundle.patch → cordis.patch.yml) and starts emitting desktop notifications on the next dsh run. Remove it with:
dsh plugin --profile demo remove notifier
Integration Notes (dsh loading mechanism)
dsh's plugin system is a two-layer model of bundle (package) + patch (config layer). This plugin package contains three elements:
1. Bundle declaration (package.json)
{
"name": "dsh-chime",
"type": "module",
"main": "dist/index.js",
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
dsh.bundle.patch tells dsh: this package contributes a config layer. Packages without this field are treated as ordinary dependencies and are not activated as plugins.
2. Config layer (cordis.patch.yml)
- insert:
- id: chime
name: dsh-chime
insert adds one plugin config line into the profile's config tree; id is the line identifier (globally unique, overridable by an upper-layer patch), and name points at the package name, which Node's module resolution uses to load the actual code.
3. Entry file (dist/index.js)
Following the dsh plugin entry convention, it exports two members:
export const name = 'dsh-chime'; // plugin name
export function apply(ctx, config) { /* ... */ } // plugin body
applyis called once config is ready;ctxis the Cordis context andconfigis theconfigfield of this package's plugin line.- The plugin only uses
ctx.on()to subscribe to events andctx.effect()to register cleanup on unload; it injects no services, so it does not depend on version details of dsh's internal services. - All registrations are reversible side effects: dsh automatically revokes listeners when unloading/hot-reloading the plugin, and pending approval timers are cleared by the cleanup function registered via
ctx.effect. - Config edits hot-reload the plugin instance (dsh's HMR), no restart needed.
Integration notes
- dsh is currently in developer preview; the official statement is that event names and interfaces may change incompatibly. Event mapping is centralized in
src/bridge.ts, and the event contract is declared in the header comment ofsrc/bridge.ts; if future event renames occur, only that file needs to be updated. - The plugin has no runtime third-party dependencies;
dist/is compiled output. When installing directly from the source directory (link:), runnpm run buildfirst (or use the shippeddist/).
Event-to-Signal Mapping
| dsh event | Signal | Notes |
|---|---|---|
session/event (turn/start) | — | records turn start time for elapsed-time stats |
session/event (turn/end, normal reason) | done | turn completed fully |
session/event (turn/end, reason.kind = 'error') | failed | turn ended in error (detail taken from reason.error) |
session/event (turn/end, reason.kind = 'aborted') | — | user aborted, no alert |
session/event (approval/asked) | blocked (delayed verdict) | approval request pending; if approval/decided (auto-adjudication / quick answer) arrives within the grace period (bridge.decisionGraceMs), no alert; the signal fires only when the deadline passes unresolved |
agent/error | failed | turn/step error (emit event; payload may lack the agent field; when session is unknown, dedupe against a global window to avoid double alerts with the subsequent turn/end failed) |
events listed in bridge.attentionFrom | blocked | escape hatch: treat extra events as "waiting for a human" as dsh event names evolve or for custom events |
session/event is the persistent session event stream; turn/* and approval/* are event types within it; agent/error is a realtime event. All signal content is extracted defensively (placeholder copy when fields are missing); no malformed event can make the plugin throw.
Configuration
Config sources, lowest to highest priority:
-
Built-in defaults (see
DEFAULT_CONFIGinsrc/config.ts) — the plugin always deep-merges with built-in defaults first, so any source only needs to write the keys it wants to change. -
Plugin-line
configfield — users override in their own profile layer'scordis.patch.ymlusing the same line id (chime). Note dsh's patch-layer semantics: a later layer wholly replaces the previous layer'sconfig(no deep merge), but keeps the line'sname. For this plugin, the line's name comes from the bundle, so users only need to give the id and the keys to set:$DSH_HOME/profiles/<profile>/cordis.patch.yml:- id: chime config: hush: armed: true from: '22:00' to: '08:00'Config edits trigger plugin hot-reload (HMR), no dsh restart needed.
-
Config file (machine-level override, shareable across profiles):
$DSH_HOME/chime.config.json, or another path via the environment variableDSH_CHIME_CONFIG. JSONC supported (comments, trailing commas). The file overrides the line config, also merged key-by-key deep:// $DSH_HOME/chime.config.json { "hush": { "armed": true, "from": "22:00", "to": "08:00" }, // only these keys need to be written "kinds": { "blocked": { "tone": { "mode": "file", "file": "~/sounds/urgent.wav" } } } }On invalid config the plugin refuses to load and prints the specific reason (fail loudly, never degrade silently).
Full config reference
{
"armed": true, // master switch
"channels": {
"desktop": true, // desktop notification channel
"tone": true, // tone channel
"bell": false, // terminal bell (BEL, interactive terminals only)
"toneProgram": "auto" // Linux audio backend: auto | canberra | paplay | aplay | ffplay
},
"lingerMs": 8000, // notification display duration (ms)
"kinds": {
"done": {
"armed": true, // done signal switch
"title": "任务完成", // title template
"message": "会话 {session} 已完成,耗时 {elapsed}", // body template
"icon": "", // custom icon path (`.ico` required on Windows)
"urgency": "normal", // low | normal | critical (Linux only)
"tone": {
"mode": "system", // none | system | file
"name": "Glass", // macOS system sound name / Linux canberra theme id
"file": "", // custom sound file (effective with mode=file; Windows: .wav only)
"volume": 60 // volume 0-100 (macOS / some Linux backends)
}
},
"blocked": { /* pending approval: default title "等待批准", urgency critical */ },
"failed": { /* run error: default title "运行出错", urgency critical */ }
},
"cadence": {
"minIntervalMs": 5000, // minimum interval between same-kind signals
"burstLimit": 8, // burst cap: max messages within the window
"burstWindowMs": 60000 // burst stats window
},
"hush": {
"armed": false, // hush window switch
"from": "22:00", // start HH:MM (local timezone)
"to": "08:00", // end HH:MM, supports crossing midnight
"weekdays": [] // active weekdays [0=Sun…6=Sat], empty = every day
},
"bridge": {
"decisionGraceMs": 600, // approval grace period (ms)
"attentionFrom": [] // extra dsh event names treated as "waiting for a human"
}
}
Template placeholders
Title and body templates support the following placeholders (unprovided placeholders are kept as-is, so typos are easy to spot):
| Placeholder | Meaning |
|---|---|
{kind} | signal-kind label (完成 / 等待批准 / 出错) |
{session} | session id |
{project} | project name (only when provided by dsh) |
{subject} | subject (e.g. the tool name that requested approval, an error summary) |
{detail} | detail (approval reason, error message) |
{elapsed} | this turn's elapsed time (done signal, and failed signals sourced from turn/end) |
{source} | the event name that triggered the signal |
{time} | trigger time HH:MM |
Command-Line Tool
The package ships a CLI (chime after a global npm install, or node dist/cli.js):
chime ping [done|blocked|failed] send a test signal (test ring)
(exit codes: 0=at least one channel succeeded, 1=no enabled channel or all failed, 2=blocked by policy/usage error)
chime probe self-check channel availability and config sources
chime config print the effective config (JSON)
chime help help
The CLI shares the same config-loading logic as the plugin (defaults + config file; note the CLI does not read the dsh plugin line's config because it runs outside dsh), so chime probe verifies the system channels the plugin will actually use.
Platforms and System Dependencies
| Platform | Desktop notification | Tone (system) | Tone (file) | Icon | Volume |
|---|---|---|---|---|---|
| macOS | osascript (built-in) | afplay + /System/Library/Sounds/*.aiff | afplay + any format | not supported (system notifications have no icon slot) | afplay -v |
| Windows | PowerShell + NotifyIcon balloon (built-in) | winmm PlaySound system sound events | winmm PlaySound, .wav only | .ico file | not supported |
| Linux | notify-send (requires libnotify-bin / libnotify) | canberra-gtk-play theme sounds | paplay → aplay → ffplay → canberra(-f) probe chain | any image format | paplay/ffplay |
- Every platform channel has an availability self-check (
chime probe); when unavailable, the reason is logged and other channels are unaffected. - Linux system tones need
canberra-gtk-play(usually bundled with GNOME desktops);kinds.<kind>.tone.namecan specify a canberra theme id (e.g.dialog-warning,complete); file tones probe in fallback orderpaplay(PulseAudio) →aplay(ALSA) →ffplay(ffmpeg) →canberra-gtk-play -f, or force one viachannels.toneProgram. - macOS system sound names: Basso, Blow, Bottle, Frog, Funk, Glass, Hero, Morse, Ping, Pop, Purr, Sosumi, Submarine, Tink.
- Windows desktop notifications are tray-balloon style (shown in the notification center on Windows 10/11). Balloons may not display in headless sessions (service accounts, SSH without a desktop) — that is system behavior.
Troubleshooting
| Symptom | Check |
|---|---|
| No alerts at all | run chime probe to see channel availability; confirm armed and kinds.*.armed are not off; confirm you are not in a hush window; look at the [chime] log lines for [已发] (sent) vs [拦截] (blocked) |
| Desktop notification but no sound | whether kinds.<kind>.tone.mode is set to none; on Linux switch backends with channels.toneProgram; whether the custom file is supported by the player (Windows: wav only) |
| Approval request did not alert | auto-adjudication happens when the policy is never or there is no responder, and approval/decided arriving within the grace period suppresses the alert (expected behavior); if humans answer very slowly, raise bridge.decisionGraceMs |
| Error did not alert | failed signals come from both agent/error and turn/end (reason.kind='error'), merged into one via session-level dedupe; if a dsh version renames events or changes the payload shape, update the extraction logic in src/bridge.ts (or add the new event name to bridge.attentionFrom, which treats it as blocked) |
| "duplicate loader entry id" at startup | usually the same plugin is mounted twice, once in the profile layer and once in the bundle layer (e.g. an old manual insert line coexisting with the dsh plugin add bundle line); delete the old manual line |
| Notification flooding | raise cadence.minIntervalMs or lower burstLimit; or just disable the done kind |
| Config change has no effect | config-file edits need the plugin hot-reload (dsh HMR) to take effect; confirm there is no syntax error (errors refuse to load and print the reason) |
Development
npm install # install dev dependencies (typescript, tsx, type packages)
npm test # run unit tests (node:test + tsx, no test-framework dependency)
npm run build # compile to dist/
Code structure:
src/
index.ts entry (name / apply, assembles the pipeline)
bridge.ts event bridge: dsh events → signals (event contract & mapping centralized here)
pipeline.ts pipeline: adjudicate → render → deliver
policy.ts adjudicate: cadence (frequency) and hush (do-not-disturb)
config.ts config: defaults, JSONC parsing, merge, strict validation
templates.ts template rendering and elapsed-time formatting
memory.ts session memory (turn start times, bounded)
desktop.ts desktop notification channel (three platforms)
tone.ts tone channel (three platforms)
runner.ts subprocess executor (command probing, timeout kill, PowerShell encoded invocation)
types.ts type definitions
cli.ts command-line entry
test/ unit tests (node:test)
License
Released under the MIT License.