dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
A multi-agent group chat for DSH that decides who is worth waking: durable message log, attention routing, and cross-workspace membership.
A multi-agent group chat for DSH that decides who is worth waking.
A durable room for several DSH sessions — plus your own — where a message is written down first, routed second, and only then allowed to wake anyone. An irrelevant message costs zero turns.
Every group chat spends its budget at delivery time: it wakes everyone, and each woken member then discovers the message was not for them. That is N turns for one message nobody needed.
This plugin moves the decision before the wake. Posting a message returns a routing plan
routing: SILENT — nobody matched, zero turns spent
or
routing: 1 recipient(s) — plan only, nothing woken
room-admin mention not-woken (dry run)
Waking is a second, explicit step, and it needs two switches to agree (see Two switches).
Three consequences that are design rules here, not accidents:
@name, or @所有人
while the room has 「允许 @所有人」 on) and the room's 「唤醒」 switch being on makes the panel
wake those people. Beyond that, the only panel action that starts a turn is 「生成群史摘要」 (also
gated on 「唤醒」 — see Room-history summaries). No name, no event:
a person typing in their own panel is not a reason to run somebody else's session.--profile desktop.dsh plugin --profile desktop add github:cilarx/dsh-attention-group
That is the whole install. It was verified end to end from a clean profile on 2026-10-17:
the command exits 0, DSH initializes the profile, and the package lands with all seven
source files, its bundle patch and its cordis.patch.yml.
The bare-name form dsh plugin --profile desktop add dsh-attention-group does not resolve
yet, because there is no npm release. Use the github: form above, or the
latest release tarball:
# download dsh-attention-group-0.1.4.tgz from the release page, then:
dsh plugin --profile desktop add "C:\path\to\dsh-attention-group-0.1.4.tgz"
Any of a directory path, a tarball, or a git URL works, because dsh plugin is package
management: it delegates to pnpm in the profile directory (run dsh plugin --profile <name>
with no subcommand and pnpm's own help comes back). What makes the package a plugin is that
package.json declares dsh.bundle.patch.
A fresh profile gets both halves of the contract written for it automatically — the dependency and the bundle layer — which is what makes the plugin actually load rather than merely be installed:
{ "dependencies": { "dsh-attention-group": "github:cilarx/dsh-attention-group" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "dsh-attention-group"] } } }
Installing by path works too, and a packed tarball is verified end to end:
npm pack --pack-destination /tmp
dsh plugin --profile some-fresh-name add /tmp/dsh-attention-group-0.1.4.tgz
# → dsh: initialized profile some-fresh-name at ~/.dsh/profiles/some-fresh-name
Afterwards the new profile's package.json contained both halves of the contract on its
own — the dependency and the bundle layer:
{ "dependencies": { "dsh-attention-group": "file:...tgz" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "dsh-attention-group"] } } }
and importing the installed copy from inside that profile succeeded, resolving
@deepseek-ai/dsh-tools from the shared ~/.dsh/profiles/node_modules. So a brand-new
profile can load this plugin — that is what "works on a bare DSH" means here.
A link: dependency and a directory junction both fail (unverified for your machine,
measured here on 2026-10-16). Node resolves imports from a module's real path, so a linked
copy outside the profile resolves @deepseek-ai/dsh-tools outside the profile too:
attention-group (dsh-attention-group): failed to import
ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/dsh-tools'
Installing a real copy inside the profile is what fixes it.
Two places in ~/.dsh/profiles/<profile>/package.json must name the package — a package
that is only in dependencies is installed but is not a profile layer, and nothing will
happen:
{
"dependencies": { "dsh-attention-group": "..." },
"dsh": { "profile": { "bundles": [ "...", "dsh-attention-group" ] } }
}
Host-side code (src/index.js and friends) is loaded once, at startup. Toggling the plugin
in the plugin panel does not re-import it; measured here, a newly added tool stayed
unknown tool across a toggle and appeared only after a restart. Install, then restart, then
look for the 「群聊」 entry in the sidebar.
Because of the real-path rule above, editing this checkout edits nothing that DSH loads.
tools/sync-plugin.ps1 copies the package into the profile and then imports the copy to
prove it still loads:
pwsh -File tools/sync-plugin.ps1
@ name are filled in. Pasting an id (session-<uuid> or a bare <uuid>) still works,
and any stable id with kind user / admin / bot / liaison makes it a name in the room.attention_group_list, then attention_send with
wake left off — you get the plan, and nobody is disturbed.Call the attention_probe tool. It reports the data root, how many rooms exist, which live
agents are visible, which optional services this deployment offers, the last five wake-up
attempts with the path each one took, the status of the panel route (registered, or the
exact reason it is not), and whether the unread prompt line registered (promptContext — a
deployment with no prompt service loses that nudge, and this says so). A missing panel that
reads as "you have no rooms" is the failure this line exists to prevent.
A room is a record in a JSON ledger plus one append-only JSONL transcript. It has an id
derived from its title (插件工程协调-fpab), a title, a charter, a member list, a routing
mode, one group-wide reasoning-effort knob, four sink settings, and a list of outgoing
liaisons.
| kind | What it is | Can it be woken? |
|---|---|---|
session | A DSH session, identified by its session id. | Yes — live or cold-resumed. |
user | The human. The panel acts as this member. | No — recorded only. |
admin | The room's built-in administrator. «总结今天»-style requests default to the first enabled admin. | No — recorded only, unless it is also a session id. |
bot | Any non-session automated participant. | No — recorded only. |
liaison | A reserved kind for an inter-room courier (see Liaisons). | No — recorded only. |
Both session-id shapes are real: session-<uuid> and a bare <uuid>. Measured 2026-10-16
against the live runtime, 008bda70-4811-4349-a1a5-d6c6ff54d7d1 is a stored, resumable session
with a resolvable route — and most sessions on the machine this was developed on look like that.
An earlier version of this plugin required the session- prefix, which quietly made roughly
seventy real sessions unwakeable; the guard now accepts both.
Only ids that look like session ids are wakeable; the panel labels each row 「可唤醒」 or
「只记录」 so a member that can never be resumed is never offered a switch that does nothing.
Waking a session means: a live session gets a follow-up; a stored session is cold-resumed
with its own provider/model and its own member preset. The preset matters — resuming
without it silently swaps the session's toolset for the deployment default (measured, and the
reason the cold path reads the session's agentPreset projection first). Reasoning effort is
the room's to decide: the member's own wakeEffort wins, then the room's defaultEffort, and
only if neither is set does the session's own last-used effort survive.
Free text on the room. It is carried into the message a woken member receives, and it is prepended to transcript exports and quoted in digests. Empty by default.
| Mode | Panel label | What it does |
|---|---|---|
rules | 规则 | Rules only: an @name mention or a subscription keyword. No match = nobody woken. This is the default. |
classify | 语义 | Rules first; whatever they could not decide comes back in the tool result as a greyZone, for the caller — the model that asked — to judge. The router never silently turns the grey zone into a broadcast, and never spends a model call of its own. |
broadcast | 全体 | Rules, then fall back to waking every enabled member. The most expensive mode; the old group-chat behaviour. |
Reasons a member is chosen (reason in every routing plan): mention (named), keyword
(a subscription term matched — plain substring, case-insensitive, or /regex/flags),
broadcast (mode fallback, or an @所有人 address), summary (a direct assignment from
attention_summarize, which bypasses relevance matching but still respects the wake switch).
A sender is never woken by its own message, and a member is woken at most once per message even when several rules match.
@所有人A message can address the whole room with @所有人 (@全体成员, @全体, @all,
@everyone work too). It is gated twice, and both gates belong to the room:
允许 @所有人 (off by default). With it off the phrase is ordinary text: the
message is still posted, and the tool result tells the sender which gate refused it.
A room where every member can wake everyone is a room nobody can afford.@所有人
stays plain text even when the switch is on.Even when it takes effect, waking still needs wake=true and the room's own wake
switch: @所有人 decides who the message addresses, never whether anyone is woken.
The boundary rules are the same Unicode ones as member names, so @所有人啊 and
@allison are not broadcasts.
A wake-up happens only when both are true:
wake=true on attention_send / attention_summarize, or
wake on a liaison.wakeEnabled), off by default.A relayed message needs its link's wake and the target room's wakeEnabled. When it
does not happen, the result says which switch was missing rather than leaving you to guess.
Two operations are deliberately outside the wake system entirely: attention_conclude and
attention_export write files and record a message, and wake nobody — sinking is not
speaking.
The panel's 「生成群史摘要」 button is also not a routed message: it is a direct request to one named member. It is the one click in the panel that can start a turn, and it still needs the room's 「唤醒」 switch — with waking off it refuses and says so (see Room-history summaries).
Waking reaches a member that is not running. The other half of "offline members do not miss
messages" is the member that is running for some unrelated reason: three messages may be
waiting in a room it has no reason to look at. Turning on 「未读写进系统提示」 ("unread in the
system prompt") — promptUnread, off by default — publishes one line into that member's
system prompt on every step, whether or not this room is why it is running:
群聊未读:「插件工程协调」3 条。用 attention_history 读,attention_send 回复。
What keeps it cheap and honest:
attention_history.systemPrompt.context()), which the
harness re-projects after compaction, so the nudge does not vanish when the conversation is
summarised.lastReadAt, so a step costs one map lookup — not a scan of a long room's JSONL. Reading the
room invalidates the cache immediately.It is a per-room switch because it is a per-step cost, paid in every step of every member of that room.
Two things, and only one of them can cost a turn:
A reminder you may ignore. 「群史摘要提醒」 ("room-history summary reminder"; summaryEvery:
关 / 200 / 500 / 2000, default 500). Once a room has that many messages that no summary
covers, the panel shows one line:
提醒:这个群已有 612 条消息没摘要(阈值 500 条)。要不要摘要由你决定 —— 插件不会自己压缩,也不会删任何历史。
It wakes nobody, writes no file, changes no history and generates nothing. It is a nudge, not a policy.
A summary you ask for. The panel's 「生成群史摘要」 button ("generate a room-history summary"). Generating one spends a model turn in someone else's session, so it happens only when a person clicks — never on a schedule, and never on its own.
What one click does:
kind is admin — through that member's own
provider and model, spending whoever installed this plugin's quota.attention_record_summary.upToSeq), the time, who wrote
it, and the text.Delivery into another session is one-way — the plugin hands over a message and never sees the answer — which is exactly why step 3 is a tool the member has to call itself.
No message is deleted. The transcript is the record; a summary is a projection of it that is
easier to read. A summarised room still has every original line, and attention_history still
reads them.
Because the flow is asynchronous, clicking does not produce a summary immediately. The panel
reports what it actually did — 「已请求,等待群管理写回」 ("requested, waiting for the room manager
to write back") — and the snapshot appears once the manager records it. The request also tells the
manager exactly how to hand the summary back (groupId, text, upToSeq), because a summary that
is never recorded is a summary lost.
Three things can stop a click, and each one is refused by name instead of silently doing
nothing: the room has no usable manager (a kind=admin member that is itself a DSH session), the
room's 「唤醒」 switch is off, or nothing has been posted since the last summary. A click can never
turn a silent room noisy by accident.
A liaison is a directed link living on the sending room: "when something happens in this
room, carry it into that room". Modes: conclusion (default — only messages sunk as
conclusions), keyword, mention, all (a firehose, use deliberately).
Loops are the whole design problem, so a forwarded message carries the trail of rooms it has passed through, and every guard is a pure function with a name:
maxHops (default 3) → refused,So A→B→A cannot happen, however the links are arranged. Refusals are always reported with their reason.
14 tools are registered by the host half.
| Tool | What it does | Key parameters |
|---|---|---|
attention_probe | Runtime diagnosis: data root, room count, live agents, optional services, the last 5 wake-up attempts, and the panel route's status. | (none) |
attention_probe_session | Whether one session can be woken: live or stored, its stored route, and what a wake-up would do. Read-only unless you ask for a real resume. | sessionId (required), resume (boolean, default false — costs tokens, it starts a real agent loop) |
attention_group_create | Create a room. Written to disk immediately; nothing is woken; the creating session joins. | title (required), charter, wakeEnabled (default false) |
attention_group_list | The rooms this session belongs to, each with this session's unread count and a preview of the newest message. Groups keep their own transcripts; nothing is pushed into your session. | (none) |
attention_group_join | Add a member, or update an existing one. Refusal here is a permission answer, not a bug: the room's joinPolicy decides. | groupId (required), memberId (required), memberName, kind, subscribe, wakeEffort |
attention_send | Post a message: append to the durable log, route, return the plan. Wakes nobody unless wake=true and the room has waking enabled. | groupId (required), text (required), wake (boolean, default false) |
attention_history | Read a room's log oldest-first and mark the reading session caught up. | groupId (required), limit (default 20, clamped to 200) |
attention_conclude | Sink one conclusion into the room's own Markdown file and append it to the room record. Never wakes anybody. | groupId (required), text (required), author |
attention_export | Write a time range of a room's transcript to a Markdown file and report the real path. Never overwrites an existing file, never wakes anybody. | groupId (required), since, until (ISO-8601), limit, path |
attention_digest | Summarise a range with no model and no wake-up: counts, who spoke how often, which conclusions were sunk, and the messages. The free half of summarising. | groupId (required), range (today / 24h / all, default all), since, until, limit (default 50, capped at 180) |
attention_summarize | Ask a member — by default the room's first enabled kind=admin — to write a summary. The ready-made digest travels inside the request, so the woken member answers in one turn. | groupId (required), range / since / until, limit (default 80), memberId, wake (default false) |
attention_record_summary | Store a room-history summary snapshot in the room record: the message number it covers, when, who wrote it, and the text. The write-back half of 「生成群史摘要」 — delivery is one-way, so the manager has to hand its summary back itself. Deletes nothing and wakes nobody. | groupId (required), text (required — the summary itself), upToSeq (optional — the last message it covers; omitted means the newest) |
attention_link | Manage the room's liaisons. Call with to omitted to list them. Directed, loop-proof, and records-only by default. | groupId (required), to, mode (conclusion default / keyword / mention / all), keywords, wake, enabled, maxHops (default 3), remove |
attention_effort | Set the room's one effort knob, or one member's override — and, with live: true, change the LIVE sessions now. | groupId (required), effort (required: off / low / high / max / ""), memberId, live (default false) |
range: "today" is local midnight to now, not UTC, because "today" is what a person
means. Explicit since/until always overrule range.
Every tool result is bounded: a tool result is replayed into every later request of the session that asked for it, so projections are capped at 200 lines, 2000 characters per line, and 60000 characters in total — and they say so when they truncate (the newest lines are kept). The durable logs themselves are never truncated — only what the model sees.
/groupYou do not have to open the panel to talk to a room. Type /group in the composer:
/group list rooms, with your unread count
/group say <room> <text> speak as yourself — nothing is woken
/group today <room> local tally of today's traffic (no model call)
A typed command never starts a turn: it writes the message and records its mentions, and waking still needs the room's own switch. Wrong room names come back as a list of the rooms that exist, not as a silent failure.
A global 「群聊」 entry in the sidebar (id attention-group, order 40) opens the panel. The
browser half owns no state: it asks the host on open and every 5 seconds, so two windows
cannot disagree about a room.
Room list (left column): total rooms and messages, total unread, 「+」 to create a room, and one row per room with its unread badge, member/message counts, whether waking is on, and the newest message's author and preview. A failed read is reported as a failure where the list would be — it is never rendered as "you have no rooms".
Chat tab: the charter, the message stream (newest last, auto-scrolled, capped at the
newest 300 messages), a 「总结今天」 / 「最近24小时」 / 「全部」 digest bar that answers without
waking anybody (and a 「收起」 button), and a multi-line composer that speaks as 「用户」:
Enter sends, Shift+Enter makes a newline (an Enter that is merely committing a Chinese IME
candidate never sends), and typing @ opens a completion list — every member name plus
「所有人」 (↑/↓ to move, Enter/Tab to fill, Esc to dismiss, or just click a row).
Whether that post wakes anybody depends on exactly two things: whether the room's 「唤醒」 switch
is on, and whether the message names somebody. Switch off — nothing happens, and the line under the
composer says so. Switch on — the @-named members are woken, @所有人 additionally needs
「允许 @所有人」, and a message that names nobody still only lands in the record. The send button
states the cost first (「发送并叫醒 N 人」) and afterwards reports who the host really woke
(「已叫醒 N 人:…」, or the failure and its reason).
Settings tab: 「群名称」, 「唤醒」, 「推理强度(群统一)」 (a slider), 「路由模式」, 「自省」, 「未读写进系统提示」, 「群史摘要提醒」 (关 / 200 条 / 500 条 / 2000 条), 「群规」, 「拉人权限」, 「结论沉淀」 (switch, real conclusion-file path, counts, sink folder, 「导出这个群的记录」, 「同时写任务记忆」), 「沟通员(对外转发)」 (per-link mode, 「停用」/「启用」, 「改为只记录」/「改为叫醒」, 「移除」, and a target picker), 「成员」 (per-member 「休眠」/「启用」, 「移出」 with a confirm step, subscription terms, per-member effort), and 「这个群的记录在哪」.
Add members by clicking, not by copying ids. After 「+ 拉人进群」, click the id box (or press 「选会话」) and the panel lists this machine's sessions with title, workspace and last-activity time, most recently active first. All three are needed, because sessions genuinely share or nearly share titles and the title alone cannot tell them apart. Clicking a row fills the session id, and — unless you already typ