Back to home

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

"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:

SignalMeaningDefault tone
doneA turn completed fully (generation finished)completion tone
blockedWaiting for a real human to approve / needing human interventionalert tone
failedA turn or request errorederror 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 (.ico required 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.patchcordis.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
  • apply is called once config is ready; ctx is the Cordis context and config is the config field of this package's plugin line.
  • The plugin only uses ctx.on() to subscribe to events and ctx.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 of src/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:), run npm run build first (or use the shipped dist/).

Event-to-Signal Mapping

dsh eventSignalNotes
session/event (turn/start)records turn start time for elapsed-time stats
session/event (turn/end, normal reason)doneturn completed fully
session/event (turn/end, reason.kind = 'error')failedturn 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/errorfailedturn/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.attentionFromblockedescape 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:

  1. Built-in defaults (see DEFAULT_CONFIG in src/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.

  2. Plugin-line config field — users override in their own profile layer's cordis.patch.yml using the same line id (chime). Note dsh's patch-layer semantics: a later layer wholly replaces the previous layer's config (no deep merge), but keeps the line's name. 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.

  3. Config file (machine-level override, shareable across profiles): $DSH_HOME/chime.config.json, or another path via the environment variable DSH_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):

PlaceholderMeaning
{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

PlatformDesktop notificationTone (system)Tone (file)IconVolume
macOSosascript (built-in)afplay + /System/Library/Sounds/*.aiffafplay + any formatnot supported (system notifications have no icon slot)afplay -v
WindowsPowerShell + NotifyIcon balloon (built-in)winmm PlaySound system sound eventswinmm PlaySound, .wav only.ico filenot supported
Linuxnotify-send (requires libnotify-bin / libnotify)canberra-gtk-play theme soundspaplay → aplay → ffplay → canberra(-f) probe chainany image formatpaplay/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.name can specify a canberra theme id (e.g. dialog-warning, complete); file tones probe in fallback order paplay (PulseAudio) → aplay (ALSA) → ffplay (ffmpeg) → canberra-gtk-play -f, or force one via channels.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

SymptomCheck
No alerts at allrun 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 soundwhether 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 alertauto-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 alertfailed 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 startupusually 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 floodingraise cadence.minIntervalMs or lower burstLimit; or just disable the done kind
Config change has no effectconfig-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.