dsh-work-done-bell
English | 简体中文
A 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_backgroundjob 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:
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:
{
"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:, notlink:. The plugin resolves@deepseek-ai/schemasteryby walking up from its own directory into the profile'snode_modules. Alink:install keeps the real path outside the profile and the import fails at load.
Uninstall
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:
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 composedshellservice. On other platforms the plugin loads and toasts, but stays silent. Porting it means changing the command inplay()toafplay/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-writefails 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 toworkspace-writestays silent;?verbose=1reports the reason inlastErrorand the resolved mode inlastPolicy. - 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:
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:
$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
No comments yet. Be the first to write one.