メインコンテンツへスキップ
I

dsh-job-notify

imnotriv3r/dsh-job-notify

Native Windows notifications when an unawaited background job settles (completed, failed, or stopped), when a conversation turn ends, or when the agent waits for a question answer or tool approval.

インストール

dsh plugin --profile web add github:imnotriv3r/dsh-job-notify

README

dsh-job-notify

English · 简体中文

npm license platform DSH node tests

Notifications for the work nobody is watching.

A background build finishes while you are refilling your coffee. The agent stops on a question while you are watching a show. This plugin brings you back at exactly those moments — and stays quiet for every command you are actually waiting on.


Contents


What you get

Six notifications, each with its own switch:

NotificationFires whenDefaultTurn off with
Background job completedA job nobody is waiting on exits 0ononCompleted
Background job failedNon-zero exit code, or the process never startedononFailed
Background job stoppedKilled by job_kill or a session archiveononKilled
Conversation turn endedYour own turn closes as completed / error / max-tokensonnotifyTurnEnd
An answer is neededThe agent blocks on ask_user_questiononnotifyQuestions
An approval is neededA tool call waits for your approvalonnotifyApprovals

Two properties matter more than the list itself:

  • It only speaks when something is unclaimed. A slow foreground command never pings you, because its caller is waiting for it. Only awaited === false settlements count as background work.
  • It observes, never decides. Question and approval notifications are read-only: the plugin always delegates with next() and cannot answer for you or change a result.

When it stays quiet

SituationWhy it is silent
Foreground shell commandsThe caller is waiting; this is not unclaimed work
A job an agent awaits with job_output(wait: true)awaited is true
Cancelled or interrupted turnsThe turn reason is not completed / error / max-tokens
Subagent and workflow sessionsTheir turns are not your conversation (origin: "subagent")
Session teardowncause: "teardown" settlements belong to unloading sessions
The plugin is disabledenabled: false cancels every subscription
The Host is not WindowsThe plugin logs one line and stays idle

The single rule behind the first two rows: only work nobody is waiting for deserves a ping. Set skipAwaited: false if you want every settlement, foreground or not.


What the notification looks like

Windows renders two short text lines: a title and a body. These are real examples of the shape each notification takes — the borders stand for the Windows banner.

Background job completed — title carries the outcome, body carries label and duration:

┌ Windows · DeepSeek Harness
│  后台任务已完成
│  npm run build · 1 分 12 秒
└ ─────────────────────────────────────────

Background job exited non-zero — the exit code goes into the body:

┌ Windows · DeepSeek Harness
│  后台任务异常退出
│  npm test · 退出码 1 · 4.3 秒
└ ─────────────────────────────────────────

Background job failed or was stopped — the failure reason takes the exit-code slot:

┌ Windows · DeepSeek Harness
│  后台任务失败
│  npm install · spawn npm ENOENT · 0.2 秒
└ ─────────────────────────────────────────

Conversation turn ended — the title is your Session's title, the body an excerpt of the answer that closed the turn. A turn that only ran tools has no answer text, so the body falls back to 本轮已完成 / 本轮达到输出上限, or 本轮出错:… when it ended in an error:

┌ Windows · DeepSeek Harness
│  dsh-job-notify
│  README 已重写,P1 可以提 PR 了 · 2 分 41 秒
└ ─────────────────────────────────────────

An answer is needed — the question comes first, with a batch count when there are several:

┌ Windows · DeepSeek Harness
│  需要你选择
│  dsh-job-notify · 先做 P1 还是 P1–P3?(共 2 个问题)
└ ─────────────────────────────────────────

An approval is needed — tool name and the reason you were given:

┌ Windows · DeepSeek Harness
│  需要你批准
│  dsh-job-notify · 工具:pwsh · 执行 git push
└ ─────────────────────────────────────────

How the text is composed

KindTitleBody
JobOne of the four fixed titles below{label} · {exit code or failure reason} · {duration}
TurnSession title → workspace folder → DeepSeek Harness{answer excerpt} · {duration}
Question需要你选择{session} · {first question} (+ (共 N 个问题))
Approval需要你批准{session} · 工具:{tool} · {reason}

Parts are joined with · and empty parts are dropped, so turning off showLabel simply removes that segment instead of leaving a gap.

Which outcome a job reports

The title is decided by exit code first, status second:

job.statusdetailOutcomeTitle
killedanykilled后台任务已停止
failedanyfailed后台任务失败
completedexit code: N, N ≠ 0nonzero后台任务异常退出
completedexit code: 0 or no exit codecompleted后台任务已完成

[!IMPORTANT] DSH's shell producer reports a non-zero exit as status: completed with detail: "exit code: N" — not as failed. This plugin therefore decides success from the exit code; trusting the status alone would announce a failed build as "completed". Both exit code: N and the [exit code: N] tool-result marker are recognised, negative codes included.

Truncation and formats

PartBudgetBehaviour
Title80 graphemesFalls back to DeepSeek Harness when empty
Job label140 graphemesFalls back to the job kind, then 后台任务
Detail, tool, reason90 graphemes eachWhitespace collapsed to single spaces
Turn excerpt200 graphemesMarkdown stripped before truncation
appId128 charactersInvalid values fall back to com.deepseek.dsh

Truncation is grapheme-safe (an emoji is never cut in half) and appends ….

Durations are humanised: 4.3 秒, 1 分 12 秒, 1 小时 5 分. Anything under 59.95 s keeps one decimal; above that it rounds to whole seconds first, so 119.9 s renders as 2 分 and never as 1 分 60 秒.


Install

Requirements

ItemRequirement
OSWindows 10 / 11
ClientDeepSeek Harness Desktop, or dsh web
HostMust run on Windows — toasts are emitted by the Host process, not the browser
Harness≥ 0.2.0-rc.1, < 0.3.0
Node.js≥ 20, no separate install needed (Harness bundles one)
NotificationsDeepSeek Harness allowed in Windows Settings → Notifications

Step 1 — get the plugin

Option A: plugin marketplace. Install it from the plugin catalogue; the package name is dsh-job-notify. It is published on npm, and @deepseek-ai/schemastery is supplied by Harness as a peer dependency, so there is nothing else to install.

Option B: a local directory. Useful for tracking main or hacking on the plugin:

git clone https://github.com/imnotRiv3r/dsh-job-notify.git E:\dshWorkspace\dsh-job-notify
cd E:\dshWorkspace\dsh-job-notify
npm install --no-package-lock

Step 2 — add it to Harness

Plugins → Add plugin → select the directory (Option B), or install it from the market (Option A).

The plugin manager writes the bundle into the profile's dsh.profile.bundles. The Host half activates immediately; the settings section appears after the next page refresh.

Desktop and dsh web share the same Plugins page. In both, notifications are emitted by the Host process, so a browser on another machine will not receive this machine's toasts.

Manual profile install

Add the dependency and the bundle to the profile's package.json:

{
  "dependencies": {
    "dsh-job-notify": "link:E:/dshWorkspace/dsh-job-notify"
  },
  "dsh": {
    "profile": {
      "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-job-notify"]
    }
  }
}

Restart Harness. The bundle's cordis.patch.yml inserts the job-notify row.

Step 3 — verify

Ask the agent to run something in the background, for example:

Start-Sleep -Seconds 5

A toast should appear within a few seconds of the job finishing. If it does not, work through Troubleshooting.

[!NOTE] A local install is link:-ed to its directory — do not delete or move it while installed. Code changes reload through HMR on the Host half; refresh the page for the browser half.

Uninstall

Remove the plugin in the Plugins panel (this drops the bundle and its settings entry), then delete the local directory if you installed one. Profile-level leftovers live in the profile's package.json and cordis.patch.yml.


Configuration

Plugins → Background job notifications — the section renders directly in the plugin's detail page. Every change is written to the profile's cordis.patch.yml and takes effect without a restart (the settings section itself needs a page refresh to appear the first time).

Master switch

FieldDefaultEffect
enabledtrueOff cancels every subscription; queued toasts may still pop

What to notify

FieldDefaultEffect
onCompletedtrueBackground job completed
onFailedtrueBackground job failed, or exited non-zero
onKilledtrueBackground job stopped
notifyTurnEndtrueOne toast per finished conversation turn
notifyQuestionstrueThe agent is blocked on ask_user_question
notifyApprovalstrueA tool call is waiting for approval

The quiet rule

FieldDefaultEffect
skipAwaitedtrueOnly unawaited jobs notify. false also notifies foreground commands — and everything an agent is waiting on

What the body shows

FieldDefaultEffect
showLabeltrueInclude the command / job label
showDurationtrueInclude elapsed time (jobs and turns)
showExitCodetrueInclude the exit code, or the failure reason for failed/stopped jobs

Advanced

FieldDefaultEffect
appIdcom.deepseek.dshAppUserModelID the toast is sent under. Letters, digits, ., _, -; max 128 chars; anything else falls back to the default

The default appId reuses the one Harness already registers, which is why notifications show up as DeepSeek Harness rather than a generic PowerShell source.

Presets

Copy one of these into the profile's cordis.patch.yml:

# Only failures — the quietest useful setting
- id: job-notify
  name: "dsh-job-notify"
  config:
    onCompleted: false
    notifyTurnEnd: false

# Jobs only, no conversation noise
- id: job-notify
  name: "dsh-job-notify"
  config:
    notifyTurnEnd: false
    notifyQuestions: false
    notifyApprovals: false

# Never miss anything, foreground commands included
- id: job-notify
  name: "dsh-job-notify"
  config:
    skipAwaited: false

# Minimal text: outcome and duration only
- id: job-notify
  name: "dsh-job-notify"
  config:
    showLabel: false
    showExitCode: false
Why every config field has to be .volatile()

The Host's settings service builds its editable form from volatile nodes only. A plain field is filtered out silently — the result is a settings section that simply never appears, with no error. Every field in Config therefore carries .volatile(), and apply reads each one through .get(). Keep this property when adding fields.


How it works

graph TD
  J["jobs.events · settled"] --> GJ{"cause ≠ teardown<br/>awaited = false"}
  T["session · turn/end"] --> GT{"reason ∈<br/>completed / error / max-tokens"}
  Q["user-questions/request"] --> GQ{"notifyQuestions"}
  A["session · approval/asked"] --> GA{"notifyApprovals"}
  GJ -->|pass| Q1["ToastQueue"]
  GT -->|pass| Q1
  GQ -->|pass| Q1
  GA -->|pass| Q1
  GJ -->|drop| S["silent"]
  GT -->|drop| S
  GQ -->|drop| S
  GA -->|drop| S
  Q1 --> P["powershell.exe → toast.ps1"]
  P --> W["Windows notification center"]

Event wiring

SourceGateResult
jobs.events (owners: 'all', settled)cause !== 'teardown'; outcome switch; awaited === false when skipAwaitedJob toast
session/event — turn/start—Records the turn's start time
session/event — assistant/message—Caches the last assistant text (Markdown stripped)
session/event — turn/endReason in completed / error / max-tokens; origin !== 'subagent'Turn toast
session/event — approval/askednotifyApprovalsApproval toast
user-questions/request waterfallnotifyQuestions; always next()Question toast
session/disposed—Drops that Session's caches

Delivery

  1. A payload is composed by pure functions in lib/format.js — no Node APIs, so the rules are unit-testable without a running Harness.
  2. Payloads enter a bounded serial queue (lib/toast.js): at most 8 wait, and a full queue drops the oldest rather than growing. The newest settlements are the ones you are most likely to act on.
  3. One toast at a time, each an independent Windows PowerShell 5.1 process — only that engine ships the WinRT projection Windows.UI.Notifications needs.
  4. The payload travels as base64 JSON on the command line, so the line stays pure ASCII and Chinese text, quotes, or % survive regardless of the console code page.
  5. The script fills the ToastText02 template and calls Show() under the configured AUMID.
  6. Each call has a 10 second deadline; a wedged shell is killed and the queue moves on.
  7. On plugin disposal the queue closes and drops whatever is still pending.

Failures never break the pipeline: the queue logs toast failed: … or toast queue full (8); dropped the oldest notification and continues with the next payload.


Privacy

  • No network requests, no credential reads, no telemetry.
  • Notification text comes from local sessions only: Session titles, answer excerpts, commands, exit codes, tool names. All of it lands in Windows notification history — assess that for sensitive projects.
  • Sending runs a local script (toast.ps1, launched with -ExecutionPolicy Bypass) that only calls the Windows toast API; it is short enough to audit in a minute.
  • Settings edits only touch the current profile's cordis.patch.yml.

Compatibility

ComponentSupported
Windows10 / 11
Harness>= 0.2.0-rc.1 < 0.3.0-0 (declared in dsh.compatibility)
Node.js>= 20
ClientsDesktop and dsh web
Peer dependency@deepseek-ai/schemastery >= 3.18.0 < 4.0.0, provided by Harness for marketplace installs

Two consequences worth knowing before you install:

  • dsh web sends from the Host. Notifications work when the Host runs on Windows and you are at that machine. A browser on another device receives none of them — the toast appears on the Host's desktop instead.
  • A bare dsh web deployment without Desktop may not have com.deepseek.dsh registered as an AUMID. If toasts never appear, set appId to an AUMID that is registered on that machine.

Troubleshooting

SymptomLikely causeFix
No notifications at allWindows notifications disabled for HarnessSettings → Notifications → allow DeepSeek Harness
No banner, but the notification center has itFocus Assist / Do Not Disturb, or a fullscreen appTurn off Focus Assist; for fullscreen, see below
No banner while watching a video in fullscreenWindows suppresses banners over fullscreen windowsPress Win + N; exclusive-fullscreen players can only be fixed by going borderless/windowed
Foreground commands notifyskipAwaited is falseSet it back to true
Background jobs never notifyonCompleted / onFailed / onKilled all off, or the job was awaitedCheck the switches; check whether the agent waited on the job
No turn notificationsnotifyTurnEnd off, or the turn was cancelled / the Session is a subagentCheck notifyTurnEnd; subagent and workflow turns never notify
No question or approval notificationsnotifyQuestions / notifyApprovals offCheck both switches. Subagents cannot start human interaction at all
Title shows the workspace folder nameThe Session has no generated title yetSend one message; the title follows
Settings section is blankA Config field lost .volatile()Re-add .volatile() — see the note under Configuration
toast failed: … in the logPowerShell blocked by security software, or the deadline hitCheck the antivirus; try running toast.ps1 manually (below)
Plugin does nothing at allHost is not Windows, or enabled is falseLook for job-notify: native toasts need Windows in the log
ERR_MODULE_NOT_FOUND on startupThe Loader row's name does not match the package nameUse name: "dsh-job-notify" in the profile patch

Limitations

  • Windows only. On other platforms the plugin loads, logs once, and stays idle.
  • Notification text is Chinese only. Titles and bodies are fixed Chinese strings; the English README describes the behaviour, not the toast copy.
  • Clicking a toast does not focus Harness. That needs a COM activator for com.deepseek.dsh.
  • Plain text only. Markdown is stripped, never rendered.
  • Exclusive fullscreen cannot be covered, and system mute cannot be bypassed.
  • The settings section depends on Harness internals (plugins.bundle.config, configForms); a major Harness upgrade may need adaptation.
  • A local install is a symlink. Deleting or moving its directory breaks the plugin.
  • Toasts belong to the Host, not the browser — see Compatibility.

Development

PathPurpose
index.jsHost entry: Config, event wiring, turn and decision notifications
lib/format.jsPure formatting and decision helpers — no Node APIs
lib/toast.jsPowerShell runner and bounded serial queue
client.jsBrowser half: the settings section in the Plugins panel
toast.ps1WinRT toast sender (ToastText02 template)
locale/Plugin-catalogue title and description (zh, en)
cordis.patch.ymlBundle patch that inserts the job-notify row
test/node:test suites
npm run check   # node --check on every source file
npm test        # node:test — 22 cases, no Harness needed

Tests never send a real toast: the PowerShell process call and the queue runner are injectable. They cover formatting and truncation, outcome detection from exit codes, Markdown stripping, the queue's ordering / overflow / close semantics, and the runner's exit-status, spawn-error and timeout paths.

Manual toast, using the same payload contract as the plugin:

$payload = @{ title = '测试通知'; body = '来自 dsh-job-notify'; appId = 'com.deepseek.dsh' } | ConvertTo-Json -Compress
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($payload))
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\toast.ps1 -PayloadBase64 $encoded

CI runs npm run check and npm test on Node 24 for every push to main and every pull request (.github/workflows/ci.yml).


Contributing

Issues and pull requests are welcome — especially platform reports, documentation fixes, and localisation. Please open an issue before a large change so the design can be agreed on first.


License

MIT

Not affiliated with DeepSeek. DeepSeek Harness is a trademark of its respective owner.

関連プラグイン