Bundle
dsh-work-done-bell
Rings a Windows system sound (and shows a bottom-right toast) when a DeepSeek Harness session has actually finished all of its work — idle plus no live background job and no running subagent.
- Source
- FunctionHookTJU
- License
- MIT
- Updated
- Updated 6 days ago
Readme
# dsh-work-done-bell
**English** | [简体中文](README.zh-CN.md)
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that
rings a **Windows system sound** when a session has *actually finished*, and
shows a **bottom-right toast** naming the session that finished. A persistent
bottom-right switch turns both off, and that preference is stored in a real
settings namespace, so it survives a restart.
## Why "idle" is not "done"
`agent/status -> idle` means only that the agent's driver has nothing scheduled.
Work routinely outlives it:
- a `run_in_background` job keeps running and its settlement opens **another**
turn, and
- a delegated subagent keeps running while its parent sits idle.
Chiming on raw `idle` therefore rings two or three times per task. This plugin
waits for **quiescence**: `idle` *and* no `running`/`stopping` job owned by the
session *and* no running agent owned by it. A 90-second deadline bounds that
wait, so a long build cannot swallow the notification; once the deadline has
rung, the later true quiescence stays silent for the same episode.
## What it sounds like
The reason comes from the durable session log (`turn/end`), not from a guess:
| `turn/end` reason | Sound | Toast |
| --- | --- | --- |
| `completed` | `Asterisk` (default notification) | yes |
| `blocked` (a plugin rejected the step) | `Asterisk` | yes |
| `max-tokens` (output ceiling hit) | `Asterisk` | yes |
| `error` | `Hand` (critical stop) | yes |
| `aborted` — including your own Esc/Stop | silent | no |
| `interrupted` (crash-orphaned turn) | silent | no |
Simultaneous completions within 2.5 s collapse into one sound, so five sessions
finishing together do not stack five players.
## Install
Clone the repository, then install it from the directory you cloned into:
```sh
git clone https://github.com/FunctionHookTJU/dsh-work-done-bell
dsh plugin --profile web add file:/absolute/path/to/dsh-work-done-bell
```
then add the package name to the profile's bundle list, in
`<DSH_HOME>/profiles/web/package.json`:
```json
{
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "...", "dsh-work-done-bell"]
}
}
}
```
(`dsh plugin add` reconciles that list itself when the installed package
declares `dsh.bundle`. Restart DSH afterwards; nothing else in the composition
needs editing.)
> **Use `file:`, not `link:`.** The plugin resolves
> `@deepseek-ai/schemastery` by walking up from its own directory into the
> profile's `node_modules`. A `link:` install keeps the real path outside the
> profile and the import fails at load.
## Uninstall
```sh
dsh plugin --profile web remove dsh-work-done-bell
```
Remove it from `dsh.profile.bundles` (which `dsh plugin remove` also reconciles),
then restart. The `work-done-bell` namespace in `settings.yaml` is inert once
the plugin is gone and can be deleted by hand.
## The switch
Bottom-right, always visible: *完成提示已开 / 完成提示已关*. It writes
`work-done-bell.enabled` through the settings service, and both halves read it —
the host half stops the sound, the browser half stops the toast. The state lives
in `<DSH_HOME>/settings.yaml`, so it survives a restart.
## Introspection
There is no model-facing tool, on purpose: `@deepseek-ai/dsh-tools` is not
present in the profile, and a plugin may only import what the profile already
carries (ESM resolution ignores `NODE_PATH`). The state route carries the same
information instead:
```sh
curl "http://127.0.0.1:3080/dsh-work-done-bell/state?verbose=1"
```
That returns tracked statuses, the last `turn/end` reason per session, open
quiescence episodes with their deadlines, the last sound exit code, and any
error the plugin swallowed. Without `?verbose=1` it returns only what the
browser half needs.
## Requirements and known limits
- **Windows only** for the sound: it plays through
`[System.Media.SystemSounds]`, executed via the composed `shell` service. On
other platforms the plugin loads and toasts, but stays silent. Porting it
means changing the command in `play()` to `afplay` / `paplay`.
- **The sound command carries a resolved sandbox policy.** A confining shell
service refuses a command that arrives without one, and the deployment
default may be a mode the host cannot enforce at all — on the machine this
was built on, `workspace-write` fails because the Windows ACL runner wants a
temp root outside the workspace while the workspace is the user profile, so
*nothing* runs confined and an unpolicied command is simply refused. The
policy is therefore resolved per chime for the session being announced
(`sandboxPolicy.resolve({ session })`), which is that session's own
authority, never a widening of it. On such a host a session confined to
`workspace-write` stays silent; `?verbose=1` reports the reason in
`lastError` and the resolved mode in `lastPolicy`.
- The toast requires an open page; the sound does not.
- The browser half polls the host every second over
`/dsh-work-done-bell/state`. There is no push channel: the sanctioned
direction of package communication is browser -> host.
- Toasts label the session by a short id, not its title.
## Development
Four suites, all runnable with no harness running:
```sh
npm test # all four
node test/state-machine.test.mjs # the decision table, on a manual clock
node test/integration.test.mjs # the plugin inside a REAL cordis app
node test/client-bundle.test.mjs # the browser bundle's contract
node test/docs-parity.test.mjs # the two readmes' shared skeleton
```
`integration.test.mjs` is the one that matters most: it boots a real
`@deepseek-ai/cordis` application with stub services and asserts that `apply`
runs, that `ctx.inject` fires, and that both HTTP routes register. An earlier
version of this plugin read `ctx.get('webServer')` once at apply time and
called `jobs.onJobsChanged` unguarded; it looked healthy while registering
nothing in production, because a host-composition row mounts *during boot*,
before its services exist.
The suites need `@deepseek-ai/cordis`, `@deepseek-ai/cordis-plugin-timer`,
`@deepseek-ai/schemastery` and `@deepseek-ai/cosmokit` resolvable from the
repository. A checkout links them from the harness installation; `node_modules`
is gitignored, so these links stay local:
```powershell
$repo = (Get-Location).Path # this checkout
$dsh = "$env:APPDATA\npm\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai"
$web = "$env:USERPROFILE\.dsh\profiles\web\node_modules\@deepseek-ai"
New-Item -ItemType Directory -Force -Path "$repo\node_modules\@deepseek-ai" | Out-Null
foreach ($p in 'cordis','cordis-plugin-timer') {
New-Item -ItemType Junction -Path "$repo\node_modules\@deepseek-ai\$p" -Target "$dsh\$p"
}
foreach ($p in 'schemastery','cosmokit') {
New-Item -ItemType Junction -Path "$repo\node_modules\@deepseek-ai\$p" -Target "$web\$p"
}
```
`docs-parity.test.mjs` guards the two readmes against drift: the same heading
skeleton, the same code blocks, the same tables, the same inline technical
terms. It cannot judge the prose, but it fails the moment one language grows a
section the other lacks.
## Layout
```
lib/index.js host half — quiescence state machine, sound, HTTP route
client/client.js client half — shell.overlay toast stack and switch
cordis.patch.yml the composition row this package contributes
test/ decision table, real-cordis integration, client + readme contracts
```
## License
MIT
Install
dsh plugin --profile web add github:FunctionHookTJU/dsh-work-done-bell
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-work-done-bell from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.