Notifications & Integrations
dsh-task-settle-notify
chenvinc/dsh-task-settle-notify · ★ 0
任务看板卡片结算后自动通知发起会话的 DSH 插件
Manifest details
安装
dsh plugin --profile web add 'github:chenvinc/dsh-task-settle-notify#2bc8e1c70bedb05ad207ea5ab431f634e032a9c6'GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
- One person, seven sites
- Without the plugin: dispatch the cards, become the dispatcher
- With the plugin: the site stays down there, conclusions come up here
- The isolation is designed, not incidental
- A standard run: four legs, all recorded
- It proved itself
- What a message looks like
- A card that stops to ask, asks here
- Requirements
- Install
- Where it gets the facts
- Verification
- Boundaries
- Compatibility
- Development
- License
The card is done — and the message knocks by itself. The session that started a card gets its settlement.
中文 · Architecture · Contract · Install · Verification

A real delivery: the card ran in its own execution session, and the notice landed in the session that started it (session id and local path redacted).
One person, seven sites
Set the scene first. You are the person in charge of this layer: one host session (your office), one task board (the site), several cards running in parallel.
Without the plugin: dispatch the cards, become the dispatcher
you: turn these three into cards, I'm stepping out
agent: dispatched (board +3, running)
—— this turn ends; the site keeps working ——
And that is where it ends. The site is running and the office is silent. There are only two ways to find out how it is going:
- come back later and ask "where are the cards?" so the agent checks the board and reports;
- or set a 20–30 minute scheduled wake-up before you leave so it comes back and looks.
Both are manual polling with extra steps — annoying, but survivable. The real pain is surprise: halfway through, a card hits something it should not decide alone — CSV or XLSX, drop that old table or not, which directory. It calls ask_user_question. But a card running on the board has nobody watching its question panel, so the card sits there waiting.
Now you open each card, read what it is asking, answer each one, and keep track of which is which. You only wanted to hand out work; you ended up with seven reports to babysit. The board delivers the parallelism, but nothing carries the results back.
With the plugin: the site stays down there, conclusions come up here
you: turn these three into cards, I'm stepping out
agent: dispatched… (this turn can end)
—— cards run, settle, and ask their own questions ——
〔board〕Q3 report export settled · succeeded · 8 min (session session-9f3c…)
artifact at out/report.csv
〔board〕Upgrade dependencies is asking you (1 question)
1. [version pin] id=q-pin1 Which major version should we move to?
Settlements and questions arrive in the initiating session like an ordinary user message, so the host session wakes up naturally: it answers what it can decide itself (answer_card_question) and only comes back to you for the rest. Everything you must know arrives; nothing you don't need does.
The isolation is designed, not incidental
- Only the initiating session: a card answers to the session that dispatched it — no broadcast, no global pop-up. You keep talking to exactly one window.
- Queued, never preemptive: a busy session finishes what it is doing; the plugin never interrupts it.
- Batching and priority: successes within 30 seconds merge into one message, failures go out at once; children do not flood, the parent summarises them in a line.
- Limits stated plainly: hand-written cards have no initiating session and therefore no return address; those are not notified. This plugin adds the return path for cards dispatched from a chat.
Dispatch seven cards and walk away; what comes back is a handful of messages, not seven reports. How many cards run, where each one is, who is waiting — none of that surfaces unless it needs your call.
A standard run: four legs, all recorded
Not a diagram. One real run on 2026-10-28 (card 846bb8a7, execution session session-2b0534b1…, initiating session session-7d8b8d2a…):
| Time | What happened |
|---|---|
| 17:25:22 | the host session dispatches the card; it starts |
| 17:25:27 | the card asks mid-run "A or B" → the plugin claims it → the question lands in the initiating session |
| 17:26:22 | the initiating session answers A through answer_card_question; the card receives {"answers":[{"id":"relay_selftest","selected":["A"]}]} |
| 17:26:28 | the card wraps up with "收到:A", 1 min 6 s of work |
| 17:26:58 | the settlement arrives in the initiating session (30 s batch window), delivered: 1, failed: 0 |
The message that arrived mid-run:
〔board〕Relay self-test: a card asks is asking you (1 question)
1. [relay self-test] id=relay_selftest This card is asking the session that dispatched it: A or B?
options: A | B
answers=[{"id":"relay_selftest","selected":[]}]
answer with answer_card_question: card_question_id="card-b29ccdfc-…", one answers item per question (copy each id verbatim).
The message that arrived at the end:
〔board〕Relay self-test: a card asks settled · succeeded · 1 min (session session-7d8b8d2a…)
next: open the board for this card
To check it yourself, look at three places: question-relayed in the plugin trace (with taskId, initiatorSessionId and the number of questions), the relayed / answered / delivered counters on GET /api/task-settle-notify/status, and agent/inbox/spliced in the initiating session's log.
That run also surfaced two design points:
- Both messages enter the session at a turn boundary, so they wait while the host is busy instead of interrupting — the question and the answer can be several turns apart.
- Order is not guaranteed. If the mid-run hint arrives after the answer, answering its id again returns
{"ok": false, "error": "…"}: the question is settled, and a duplicate answer never lands in a card that already finished.
It proved itself
On the first real acceptance run, the card waiting on the board was this plugin's own wrap-up card. When it settled, the thing that carried "I'm done" into the session was this plugin — reporting its own completion.
The line of thinking under the screenshot keeps the moment: The plugin card settled, and its own final message came via the plugin — nice recursive proof.
Once it settles in, manual polling and fallback reminders can go: with the plugin installed, a scheduled self-check is just noise.
What a message looks like
Single card, success:
〔看板〕M2-B 已结算 · 成功 · 用时 9 分(会话 session-9f3c…)
产物在 src-tauri/src/kp_document.rs
Failure (never batched, sent immediately):
〔看板〕构建 Linux 包 已结算 · 失败 · 用时 2 分(会话 session-9f3c…)
原因:cargo build failed: linking with `cc` failed
下一步:查看 ~/.dsh/dsh-task-settle-notify/trace.log,或重跑该卡
Several successes inside the window:
〔看板〕3 张卡已结算 · 成功 · 用时合计 21 分(会话 session-9f3c…)
· 修 K 线导出 · 8 分 · 产物在 src/kline.rs
· 补任务看板文档 · 6 分 · 下一步:打开看板查看该卡详情
· 升级依赖 · 7 分 · 产物在 package.json
A settling parent card adds one line only: 含 4 个子任务:导出、导入、去重、文档.
The samples are verbatim output: message text is currently Chinese only, while the settings card is bilingual. See Boundaries.
A card that stops to ask, asks here
A card does not only need you when it settles. Halfway through it may need a decision, a file path, or confirmation of something destructive — it calls ask_user_question, and a card running on the board has nobody watching its question panel.
The plugin claims that question and sends it into the session that dispatched the card:
〔看板〕季度报表导出 在推进中向你提问(共 1 问)
1. [导出格式] id=q-export-format 导出成 CSV 还是 XLSX?
选项:CSV | XLSX
answers=[{"id":"q-export-format","selected":[]}]
用 answer_card_question 回答:card_question_id="call-…",answers 里每道题给一项(id 照抄题干里的 id)。
The id= on each line is the upstream questions[].id, verbatim — never regenerated or clipped — and the answers= line at the end is the skeleton to fill in. answer_card_question matches by id, so a relay without ids cannot be answered at all. For the rare upstream shape with no id the message says id 未知(upstream 未提供) and tells the reader it cannot be answered by tool — the GUI panel has to answer it.
That session answers when it can; when it cannot, it asks you and hands your answer back. The card carries on as if it had received a normal answer. If nobody answers before the deadline the card is told "unanswered — continue the conservative way and record the open point", so it is never stuck forever.
Both knobs live in the settings card: Relay card questions (on by default) and Question wait limit (10 minutes by default, 60–3600s).
Requirements
The market has plenty of task-board plugins, and most of them cover the dispatch side: how cards are created, queued and run, and how the board looks. This one does none of that — it does not dispatch, execute, or write the ledger. It subscribes to a single event, "a card settled", and carries the result back to the session that asked for the card. So it needs a board that can already create and run cards.
| Requirement | Version | Without it |
|---|---|---|
| DSH host | ≥ 0.2.1-rc.2 |
Older hosts do not read dsh.bundle, so the plugin is never loaded |
| Task board | @linxin666/dsh-client-ui-task-board ≥ 0.4.8, provider extension v1 |
No taskBoard service means the plugin idles: extensionActive: false in status, no errors, no effect on the board |
| Session gateway | the host's own typertGateway |
Delivery fails into error.log and is retried at most once |
| Node | ≥ 20 | It will not install or run |
@deepseek-ai/cordis |
~4.0.5 (peer) |
Supplied by the host, never bundled |
The board is a hard requirement, but it is not in this package's dependencies: the plugin imports nothing from the board, never reads ~/.dsh/task-board/ledger-v2.json, and never patches the board package. Any board implementation that exposes the same taskBoard service and the same registerExtension / onExecutionSettled contract will work — swapping boards needs no change here.
It registers one extension subscription and only reads events, so it can be installed alongside other task-board plugins. Once the above is in place, continue to Install.
Install
It is on npm as dsh-task-settle-notify. Two commands:
dsh plugin --profile web add dsh-task-settle-notify
# then add "dsh-task-settle-notify" to dsh.profile.bundles in ~/.dsh/profiles/web/package.json
To follow the source instead, clone it and use the script (it does both steps, plus a backup and a receipt):
git clone https://github.com/chenvinc/dsh-task-settle-notify.git
cd dsh-task-settle-notify
node scripts/install.mjs
The script backs up the profile files it will touch, then link:s the plugin into dsh.profile.bundles. It does not touch the DSH source, the board package or the ledger. A profile with live reload hot-mounts it; otherwise restart dsh web once. Verify:
curl -s http://127.0.0.1:3080/api/task-settle-notify/status
# {"ok":true,"status":{"extensionActive":true,"pending":0,"delivered":2,"failed":0,...}}
extensionActive: true means it is wired up. Uninstall: node scripts/uninstall.mjs.
The settings card lives under Settings → Task settlement notifications with four switches: master switch, batching window (30 s by default), only cards started by a session, and whether the parent summarises its subtasks. You can also edit ~/.dsh/profiles/web/task-settle-notify/settings.json directly.
Where it gets the facts
Every time a card settles, the board broadcasts the settlement to its subscribers. This plugin subscribes to that public event (no DSH patch, no board patch, no ledger writes), looks up which session started the execution, dedupes, applies the parent/child rule, batches, and queues the message into that session with session/prompt.
No polling, no file watching, and no turn is ever interrupted. For the exact contract and file line numbers see docs/ARCHITECTURE.md.
Verification
- 60 unit tests,
node --test, all green, no real board needed. - On a real host: settlement-to-delivery interval 30.012 s, exactly the configured window.
- The message really landed:
agent/inbox/spliced(seq: 1472) in the initiating session's durable log, withstatuscountingdeliveredup andfailed: 0. - The whole relay chain (claimed → delivered → answered → received → settled) is the run above, "A standard run".
Full record — ids, timestamps, log excerpts, reproduction steps — in docs/VERIFICATION.md.
Boundaries
| Limitation | Detail |
|---|---|
| Same Host and profile only | A deleted or remote session fails into error.log |
| Scheduled runs are not notified | A cron run has no initiating session to answer |
| Hand-written or otherwise unsolicited cards are not notified | There is no initiating session, so there is nowhere to deliver |
| Delivered ≠ read | The host queues the message; a permanently busy session keeps it queued |
| No replay | Only settlements that happen while the plugin is running are reported; the question relay has the same lifetime |
| The relay takes the question over | A claimed question never reaches the board or GUI question panel; it only goes to the initiating session |
| Message text is Chinese only | The settings card is bilingual; the message copy is not localised yet |
| Free-text answers come from the model | The relay does not parse prose: the initiating session returns structure through answer_card_question |
| A question without an upstream id is not claimed | No id is invented: the message says so, the card's own panel keeps waiting, and the initiator gets a notice |
The full list, including host-level edges such as subagent sessions being unable to receive, is in docs/ARCHITECTURE.md.
Compatibility
Tested with DSH 0.2.1-rc.2, board 0.4.8 and Node 24.19.0 on macOS. Pure Node, no native dependencies; other platforms are untested but should work. What it needs is listed under Requirements.
Development
npm test # node --test, 60 tests
index.js client.js lib/ (10 modules) locale/ test/ scripts/ docs/
Module responsibilities, data shapes and self-healing strategy: docs/ARCHITECTURE.md. The field-level contract with the board: docs/CONTRACT.md.
License
MIT © chenvinc
Community plugin, not an official DeepSeek project; names such as "DeepSeek Harness" belong to their respective owners.