# dsh-task-chime English | [中文](README.zh.md) A plugin for **DeepSeek Harness (DSH)** that plays a real **operating-system sound** every time an agent turn finishes — so you can look away from the screen during a long task and still know the moment it is done. Unlike a browser notification, the sound is produced by the Host process through the system audio device, so it reaches you when the DSH window is minimized, in the background, or on another virtual desktop. ``` turn ends ──► agent/status: idle ──► ctx.shell ──► 🔔 C:\Windows\Media\notify.wav ``` ## Features | | | |---|---| | **Real system sound** | Played by the Host through `ctx.shell` (PowerShell `System.Media.SoundPlayer` / `[console]::Beep` on Windows), not by the web page — audible while DSH is in the background | | **Two distinct moments** | *Task finished* and **“the agent is blocked on you”** (a question or a pending approval), each with its own sound and intensity so you can tell them apart by ear | | **13 sound choices** | System effects (`notify`, `ding`, `chimes`, `tada`, `chord`, `calendar`, `messaging`, `exclamation`, `alarm`, `ring`), two pure beep patterns that need no audio file, plus **any custom audio path** | | **4 intensity levels** | L1 light (1 play) · L2 standard (2 plays) · L3 strong (rising prelude + 3 plays) · L4 alarm (two-way prelude + 5 plays) | | **Auto escalation** | A task over 2 minutes rings one level higher; over 10 minutes, two levels higher (capped at L4) | | **Trigger scope** | Top-level sessions only (default), or include subagent sessions | | **Duration threshold** | Stay silent for turns shorter than *N* seconds, so quick answers do not beep | | **No spurious chimes** | A completion ring requires an observed `running → idle` transition, and an approval rings only if it is still pending after a grace period — so opening a session or an auto-answered policy check stays silent | | **`task_chime` tool** | An optional model tool, so you can just say "ring me" and the agent triggers a chime on demand | ### Why the second moment needs its own trigger While the agent waits for you — `ask_user_question`, `exit_plan_mode`, an approval prompt — its status stays `running`. `agent/status` never reaches `idle`, so the completion chime **cannot** fire. Without a separate trigger, the one moment that actually needs you is the one moment that makes no sound. ## Two ways to run it The repository ships **both halves of the same feature**. Pick by how long you want it to live. | | **A · Profile bundle** | **B · Dynamic plugin** | |---|---|---| | Entry | `lib/index.js` + `cordis.patch.yml` | `src/host.js` + `src/client.js` | | Install | `dsh plugin add`, then restart | `cordis_define` + `cordis_run`, no install | | Survives a DSH restart | **yes** | no | | Applies to every session / project | **yes** | that one session | | Configuration | **a card in Settings → Plugins**, `settings.yaml` (namespace `task-chime`), or the composition entry | in-memory, lost on restart | | UI | a **settings card** in Settings → Plugins | a panel in Settings and in the `cordis_run` card, additionally offering sound preview, temporary mute, and a chime log | | Needs approval | no | yes (a Client half always asks) | Mode A is the one to install. Mode B stays useful for trying a change instantly, or for the richer panel. ## A · Install as a profile bundle (permanent) ```sh dsh plugin --profile web add github:ruazero/dsh-task-chime ``` Then **restart the Harness** (quit and reopen DSH Desktop, or restart your `dsh web` process). That is all: the package declares `dsh.bundle.patch`, so its composition row is applied automatically — you never edit a composition file by hand. Verify before restarting, without starting anything: ```sh dsh --profile web --dump-config | grep -A2 task-chime ``` To remove it: ```sh dsh plugin --profile web remove dsh-task-chime ``` ### Configuration Three layers, in order of authority: 1. **The settings card** — *Settings → Plugins → Task completion chime*. Every control writes immediately; a changed field shows a *customized* badge whose **Reset** removes the override instead of writing a default over it. 2. **`settings.yaml`, namespace `task-chime`** — the same durable document the card writes, watched live: a hand edit takes effect on the next turn without a restart. 3. **The composition entry** in `cordis.patch.yml` — the base layer, and the fallback whenever no settings service is mounted. ```yaml # settings.yaml task-chime: enabled: true # master switch for every chime sound: notify # notify | ding | chimes | tada | chord | calendar | # messaging | exclamation | alarm | ring | # beep-triad | beep-low | custom customPath: '' # absolute audio path, used only when sound is `custom` level: 2 # 1 light · 2 standard · 3 strong · 4 alarm autoEscalate: true # +1 level past 2 min, +2 past 10 min minDurationSec: 0 # stay silent below this turn duration scope: roots # roots = top-level sessions only · all = include subagents registerTool: true # expose the on-demand `task_chime` tool # ring when the agent is blocked on you notifyOnInput: true # questions and pending approvals inputSound: messaging # same catalog; keep it different from `sound` inputLevel: 3 # its own intensity inputTools: # tools that block until a human answers - ask_user_question - exit_plan_mode approvalDelayMs: 1200 # grace period; a policy-answered approval stays silent ``` To pin a value in the composition instead, override the row by id in your profile's own `cordis.patch.yml`: ```yaml - id: task-chime config: level: 3 sound: chimes ``` ### Intensity levels | Level | Prelude | Plays | Gap | |---|---|---|---| | L1 light | — | 1 | — | | L2 standard | — | 2 | 0.22s | | L3 strong | rising triad (C6–E6–G6) | 3 | 0.4s | | L4 alarm | rising + falling triad | 5 | 0.65s | ## B · Run as a dynamic plugin (this session only) ```sh git clone https://github.com/ruazero/dsh-task-chime.git ``` Then paste this into a DSH session that has the Cordis tools (the shipped `cordis` preset does): > Read `src/host.js` and `src/client.js` from ``, then call `cordis_define` with `plugin.kind: "new"`, `idPrefix: "chime"`, `code.host` = the full contents of `src/host.js`, and `code.client` = the full contents of `src/client.js`. Then activate it with `cordis_run` in `run` mode. Approve the run when the card appears. This mode adds a **custom control panel** — sound picker with preview, the four intensity buttons, trigger scope, duration threshold, temporary mute (10/30/60 min), and a log of the last 12 chimes — in **Settings → 任务提示音** and inside the `cordis_run` card. Each `src/*.js` file is the plain-JavaScript **function body** the dynamic evaluator expects — it ends with `return { apply(ctx) { … } }`. Do not wrap it in a module and do not add `import`/`require`: neither exists in that sandbox. Details and troubleshooting: [INSTALL.md](INSTALL.md). ## How it works ### Task finished 1. The Host listens to the event `agent/status`. It fires `running` ⇄ `idle`; `idle` means no driver remains scheduled or active — the turn is genuinely over, not merely between steps. 2. A start timestamp is recorded on `running`. On `idle` the plugin computes the task duration and applies, in order: the enable flag, the trigger scope, the duration threshold, and long-task escalation. **A ring requires a previously observed start**, so a resumed session never rings on its own. 3. It builds a small shell script — `SoundPlayer.Load()` plus repeated `PlaySync()`, or `[console]::Beep` patterns — and runs it through `ctx.shell.resolve()` + `ctx.shell.run()`, fire-and-forget: the agent never waits for the sound. 4. The sandbox policy is resolved from the completing session. Since the command writes nothing, a `read-only` session is raised to `workspace-write` for the single purpose of keeping PowerShell in FullLanguage; nothing wider is ever requested. 5. Every capability is probed rather than assumed. No `shell`, no `agents`, no `settings`, or no `tools` degrades one feature instead of failing the plugin row. ### Blocked on you 6. A **question** is caught on the `tools/pre-execute` waterfall: when the pending call's name is in `inputTools`, the input-request sound plays and the decision is delegated untouched — the plugin never influences whether a call is allowed. 7. A **pending approval** is caught on the `approval/request` waterfall. Ringing immediately would fire for approvals a policy answers on its own, so the plugin arms a timer for `approvalDelayMs` and clears it in a `finally` block: only a request still open after the grace period — i.e. genuinely waiting for you — makes a sound. Armed timers are cleared on fiber teardown, so none outlives the plugin. ### The settings card The shipped Plugins settings section enumerates the settings namespaces the Host serves and dispatches `settings.plugin.item` **keyed by each namespace**, rendering whichever card claims that key — a served namespace that no card claims renders nothing at all. The Host half registers the namespace `task-chime`, so `lib/client.js` claims exactly that key and the card appears beside the shipped shell and agent-loop cards. `lib/client.js` is a browser module written directly in the client wire format (`window.__ModuleLoader__.load({ id, factory })`) rather than produced by a bundler: it requires nothing but `react`, so a build step would add a toolchain without adding behaviour. Writes go through the bound settings scope (`ctx.settingsScope.bind({ namespace: 'task-chime' })` → `set` / `unset`), which is the same durable document the Host half watches — so there is no private RPC between the halves and no second source of truth. ## Platform support | | Status | |---|---| | **Windows 10/11** | **Verified.** PowerShell `System.Media.SoundPlayer` + `[console]::Beep`, sounds from `C:\Windows\Media\` | | macOS | Best-effort, unverified: `afplay` with `/System/Library/Sounds/*.aiff`, `osascript -e beep` | | Linux | Best-effort, unverified: `paplay` (falling back to `aplay`) with `/usr/share/sounds/freedesktop/stereo/*.oga` | A report from macOS or Linux is a bug, not an expected limitation — please open an issue. ## Developing ```sh npm test # 25 checks, silent: no sound is played ``` `test/smoke.mjs` (17) drives the Host `apply()` against a fake Cordis context and asserts the exact command shape per level, custom-path quoting, the no-observed-start guard, scope filtering, the duration threshold, sandbox-policy handling, live settings precedence, and every degraded path. `test/client.mjs` (8) loads the browser module the way the module loader does and checks the slot registration contract, that each control writes its own field through the settings scope, that a reset clears an override instead of writing a value, that a read-only or still-loading document locks every control, and that the card renders under real React. Both halves resolve their dependencies from the host profile. For a local run, link them into `./node_modules` (git-ignored): ```powershell # Windows example; point at your own profile's node_modules $profile = "$env:APPDATA\dsh-desktop\harness\profiles\node_modules" New-Item -ItemType Directory node_modules\@deepseek-ai -Force foreach ($m in 'schemastery','dsh-tools','dsh-settings') { New-Item -ItemType Junction "node_modules\@deepseek-ai\$m" -Target "$profile\@deepseek-ai\$m" } foreach ($m in 'react','react-dom','scheduler') { New-Item -ItemType Junction "node_modules\$m" -Target "$profile\$m" } ``` ## Limitations - **Mode B is ephemeral.** A dynamic plugin and its configuration live in memory for the life of the DSH process. Mode A is the permanent form. - **No sound preview in the card.** Previewing would need a Host call from the browser, which a composed plugin can only do through a published Remote service; ask the agent to call `task_chime` instead. - **`registerTool` needs a restart.** It is read once when the row activates, so toggling it in the card takes effect on the next Harness start. Every other field applies to the next turn. - **`PlaySync` blocks its own child process** for the duration of the sound — by design, since that is how the repeat gaps stay accurate. It never blocks the agent. - **One sound for every outcome.** Success, error, and "waiting for your input" all ring the same way today. ## Roadmap - Verified macOS / Linux backends - Per-outcome sounds (success vs. error vs. awaiting input) - Optional Windows toast notification alongside the sound - Temporary mute and sound preview in the composed mode (mode B already has both) ## License [MIT](LICENSE)