DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Handoff Bus — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-handoff-bus

Handoff Bus

用于 DeepSeek Harness GUI 对话的跨会话消息总线:在主对话与子对话之间直接传递指令和报告。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-handoff-bus@0.4.3
README兼容性版本

兼容性与来源证明

Handoff Bus 以 dsh-handoff-bus 发布,当前版本为 0.4.3。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
npm
Registry 更新时间
2026/10/19

版本

0.4.3stable
2026/10/19
0.1.1stable
2026/10/19

相关插件

正在加载相关插件…

最新版
0.4.3
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
192.9 kB
文件数
11
Surface
any
许可证
MIT
发布源
npm
GitHub
★ 0
周下载
0
最近提交
2026/10/19
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 integrations-communication 分类下经过校验的插件。

Acp App@deepseek-ai/dsh-acp-appdsh ACP 配置文件包:基于 dsh-base 的仅限自动化的 JSON-RPC stdio 和进程生命周期管理Experimental Plugin Market Bundle@deepseek-ai/dsh-experimental-plugin-market-bundle实验性自定义插件市场:用于浏览和安装来自插件市场索引的捆绑包的“发现”选项卡Workbuddy Connectdsh-workbuddy-connect将 WorkBuddy 桌面 App 中包含的模型自动接入 DeepSeek Harness,无需配置。Ab Wechat Scrapedsh-ab-wechat-scrape--- description: "树外 wechat_scrape 工具:获取微信公众号文章,过滤推广区块,并将每篇文章保存为 Markdown 文件,供此机器的 dsh 配置文件维护者使用。" kind: "package-reference" ---

README

dsh-handoff-bus

English | 中文

A small host plugin for DeepSeek Harness (DSH): it lets the main conversation and the sub-conversations you open yourself send instructions and reports to each other directly, so you no longer have to switch windows and relay messages.

What it does

  • ✅ Main → line: bus_send(target="api", kind="指令", done="…", ...) — write only the line name and it lands on whichever conversation under that line is alive
  • ✅ Line → main: bus_send(target="main", kind="回报", ...)
  • ✅ Three levels: main / line api / temporary conversations api.1, api.2; replacing a conversation never changes what the main conversation writes
  • ✅ Scoped by session cwd: each workspace gets its own independent bus
  • ✅ Remembers three things for you: a start-up briefing in every new conversation, a context-watermark notice at 40%, and a refusal to unregister without writing the ledger

What it does not do

  • ❌ It does not create conversations. You open them in the GUI; the plugin only registers and routes.
  • ❌ It does not close conversations. It gives you the signal (suggestHandoff); closing is your call.
  • ❌ It does not compress context. Compaction is triggered by the host; the plugin can neither read the water level nor change the threshold.
  • ❌ No file watching, no polling, no autonomous work when you are away
  • ❌ It does not replace your state files: task files and reports are still written by the conversations; the bus only delivers pointers and wakes the target
  • ❌ No cross-workspace addressing (one bus per workspace)
  • ❌ No addressing of subagent sessions: only a top-level GUI conversation can be a target. A session running as a subagent under another agent is owned by the host router; registering it from inside does not make it reachable, and bus_send answers target_unresolvable

How it works

you ──open the main conversation──▶ main (bus_register role="main")
                                     │
                                     │ 1. write task files / instruction files
                                     │ 2. bus_send(target="api", kind="指令", done="…", message="…")
                                     ▼
                                  api line ──▶ api.1 (bus_register parent="api")  ← woken
                                     │ 3. do the work, write the report file
                                     │ 4. bus_send(target="main", kind="回报", ref="R-…")
                                     ▼
                                  main (woken once per few reports)
                                     │ 5. api.1 stays and waits; only when its context is full
                                     │    (or the user is done) does it unregister, and api.2 takes over
                                     ▼
                                  api.2 (bus_register parent="api")

Keep the bus message short: summary + file pointers. The authoritative content stays in your files.

Requirements

  • A DSH web/desktop profile with the Host session controller and session store. Headless/SDK-only profiles may not provide them; the plugin then stays inactive (it declares inject).
  • Tested on DSH 0.2.0-rc.x (desktop, Windows). Host services such as sessionController.resolveAgent, agent.followup/inject, sessions.flush, session.header.cwd, the agent/created event and the agent/pre-step hook are internal APIs, not a public contract; a future DSH release may require a patch. If the hook throws, the plugin degrades to "no advisory" and the turn proceeds untouched.
  • No npm dependencies. The plugin imports only Node builtins.
  • Tool result messages and relayed message bodies are in Chinese; error codes are stable English identifiers. Role names are lowercase ASCII with dots (main, api, api.1).
  • Context water level comes from the host: sessionQuery.observeSession counts compactions from the event log, sessionProjectionCache.cachedSnapshot reads the contextTimeline bucket sums and window. Both are internal services; when they are unavailable the plugin falls back to a character estimate and flags it estimated: true. bus_list.contextPct only appears for conversations that have been measured.
  • No session creation or closing: both are GUI-only actions.

Install

From npm (recommended, once published)

  1. DSH sidebar → Plugins → Add plugin → search for dsh-handoff-bus
  2. Install, then Enable
  3. Fully quit the client (including the tray) and reopen it — this is a host plugin

Or:

dsh plugin --profile <profile> add dsh-handoff-bus

The CLI command targets a CLI-booted profile. In the desktop app the profile is managed by the app itself, so install through the Plugins page there.

From source

  1. Clone this repository.
  2. Plugins page → Add plugin → enter the absolute path of the repository directory.
  3. Enable, then fully quit and reopen the client.

A linked plugin lives outside the profile's node_modules tree and therefore cannot resolve @deepseek-ai/*. This package intentionally imports only Node builtins, so both install methods work.

Quick start

  1. Main conversation: bus_register(role="main")
  2. Each sub-conversation: bus_register(parent="api") → gets api.1
  3. bus_list shows the role tree and where each line would route
  4. Main → line: bus_send(target="api", kind="指令", done="…", message="…")
  5. Line → main: bus_send(target="main", kind="回报", ref="R-001", message="…")

Role names are lowercase letters, digits, dots, underscores and hyphens, at most three levels (main / api / api.1 / api.1.worker).

ref is passed through verbatim — nothing validates its format. The convention in the examples is D- for instructions and R- for reports (Q- for status requests), numbered per project. Pick one scheme and keep it consistent; it only lands in the message header, so the receiving conversation is what gives it meaning.

SituationCall
Main gives work to a linebus_send(target="api", kind="指令", done="npm test passes", message="…")
Sub reports backbus_send(target="main", kind="回报", ref="R-…", message="…")
Stuck, main must act nowsame, plus urgent: true
Main asks for status (no work)bus_send(target="api", kind="请求", ref="Q-…", message="Status only, do not start work.")
Queue without wakingadd wake: false

Three-level roles

LevelExamplesWho maintains itRotate?
1 mainmainRegistered once by you in the main conversationNo. There is exactly one per bus
2 lineapi, web, docs, dataAppears automatically as a virtual node the first time a child registers under itNo. It is only a name; no session is attached
3 temporary conversationapi.1, api.2Numbered by the plugin when a new conversation registersYes: one task per conversation, unregister when done, open a new one next time

Registering

bus_register(parent="api")                        → role: "api.1"
bus_register(parent="api", note="calibration")    → role: "api.2"
bus_register(role="main")                         → the main line
bus_register(role="api")                          → same as parent="api": you get api.N (see below)

All you say in each new sub-conversation:

注册 api

The plugin sends that conversation a fixed three-step note on its first request (bus_register(parent="api") → read handoff/api/ → report), so the model never needs to know which number is next, nor guess between role and parent.

A line name never becomes a conversation. role="api" used to bind the line itself, after which routing short-circuited to it and the numbered conversations never received anything. A bare line name passed as role is now coerced to parent, and the result says so via coercedFrom.

Registration also tells you where you stand: if a smaller-numbered conversation is already on the line, you are a substitute and should stand by. That is stated, not enforced — the queue order is untouched, the lowest number always wins.

How a target is resolved

  1. target="api.2" → that exact conversation, no guessing.
  2. target="api" → the line queue: candidates are the conversations that are not retired and hold no unfinished instruction, sorted by number, and the lowest number wins. Each is opened in turn until one resolves (at most routeMaxAttempts, default 3).
  3. Nobody usable → line_busy (there are conversations, all working) or no_available_session (all retired, or the line is empty).

The result carries routedFrom / routedTo, so nothing is a black box. bus_list.routeTo tells you where it would land before you send.

A line owns one instruction at a time. A bench is not a parallel pool: while api.1 owes a 回报, api.2 does not sneak in — the send is refused, and naming api.2 explicitly does not help either. There is no force; the only override is preempt: true, which injects "stop and save, then switch" into the running turn and cancels the displaced instruction.

Handing a line over

bus_register(parent="api", replaces="api.1")

The old conversation moves to retired (kept for retiredRetentionDays, default 7, so a "why can't I send here" question has an answer). If it still had an unanswered instruction, the result names that ref and tells the new conversation to read the ledger first instead of redoing the work from scratch.

When to archive a temporary conversation

The bus now decides this from measured context (compaction count first, water level second) and marks the conversation retired when it is idle:

advisestop taking work
compactions≥ 1≥ 2
water level> 50%> 70%

A retired conversation keeps its context and its place in the tree; routing simply skips it. The bus does not lecture the user from the outside — it drops one line into the retired conversation's own history, and that conversation tells its own window: "archive me, the next one will be api.4".

suggestHandoff still exists as a softer hint (40 messages, or 30 minutes idle) for conversations that are merely stale rather than full. bus_list also exposes compactions and contextPct once a conversation has been measured.

When the main conversation has to go

The main conversation fills up too, but its line is wider: **≥ 2 compactions or

85%**. Stopping it stops the whole workspace, so the cost is not the same as swapping a temporary conversation.

It is never cut short. It drains:

  • it stops issuing instructions (draining is returned) and keeps receiving reports;
  • no line is interrupted — they finish at their own pace;
  • a background sweep checks every 900s whether every line is idle, backing off to 1 hour after 3 quiet rounds, because a long task does not deserve to be poked;
  • the moment everything is idle, it says: write the ledger, archive me, open a new main and say "take main".

A new main calling bus_register(role="main") takes the name over; child bindings and numbers are untouched, and their reports land on the new main automatically.

Being compacted while draining is harmless — at that point it only has to remember one word: wait.

Rule of thumb: one tool call adds roughly 2–3k tokens, so 150–200 tool calls lands around 400k. A line is ready to hand off after 8–10 turns, before it starts forgetting things.

Instructions must carry an acceptance test

bus_send(kind="指令") without done is refused (instruction_incomplete):

bus_send(target="api", kind="指令",
         message="Split the export logic into its own module",
         done="npm test passes; the exported bytes are identical to the old implementation")

done is echoed in the delivered message so the report can be checked against it. This removes the most common waste: an instruction without an acceptance test makes the sub guess, a wrong guess produces a follow-up instruction, and every follow-up costs the target a whole context reload.

Disable with requireDone: false.

Report digest and urgent

Not every report deserves a wake-up — the main conversation usually holds the longest context, so its wake-ups are the most expensive.

CaseDefault
kind="回报" without urgentNo wake. The message goes into the main conversation's history; after digestMinCount reports (default 3) or digestMaxWaitSec (default 900s), one summary message wakes it once
kind="回报" + urgent: trueWakes immediately
kind="请求"Urgent by default (a request is a decision ask)
wake: true / wake: falseExplicit override, highest priority

Only three things deserve urgent: stuck, failed and needs a new approach, needs a decision. "Done", "50%", "here is the artifact path" do not.

An urgent message folds any queued backlog into the same wake-up (the text says "N earlier reports are already queued") instead of waking twice. Urgent deliveries still obey the rate limits.

Implementation note: a batched report is injected into the history without waking, so a client restart loses the wake-up, never the message. The single timer exists only while a digest is pending and is unref'd.

Disable with digestReports: false (back to waking on every report).

Delivering without piling up

Every delivery is a separate turn in the target conversation. If main sends three 指令 to worker-a in a row while worker-a is still working, all three queue up and are executed in order — the first one runs against a task file that is already at version three. Three consequences:

  • A second 指令 is refused while the line still owes a 回报 (line_busy). Update the task file, wait for the report, then dispatch again. There is no force and no timeout: a long task keeps the line. To take the line anyway, preempt: true — it injects into the running turn, orders "stop and save the half-done work", and cancels what it displaced. A late report for the cancelled ref is recorded but never frees the line.
  • Each 指令 carries its own sequence ("本线第 3 条指令") and the footer tells the receiver to compare it with bus_list.latest. A seq below the latest means the message was superseded: do the newest one, skip the duplicate work.
  • The counter and the pending state are rebuilt from deliveries.jsonl, so a client restart neither resets the numbering nor forgets an unanswered instruction.
  • Numbering follows the actual target, so target="api" and target="api.2" landing on the same conversation share one sequence and cannot be used to bypass the guard.

A sub-conversation prompt that makes this reliable:

收到 [BUS 指令] 后,先调用 bus_list 看 latest 里「main->自己角色」的 seq。
如果消息末尾写的「本线第 N 条」小于 seq,说明已被后面的指令取代:
只做最新那条,不要重复劳动,并在回报里说明跳过了哪一条。

Context budget and start-up briefing

The longer a conversation lives, the more every request costs. The three things below are what a person would otherwise have to remember; the plugin enforces them.

WhenWhat happensDefault
Before a conversation's first requestA briefing is injected: which handoff files to read, and how to read the big onesbriefing: true
Context crosses the watermarkA budget notice: finish the current task, write the ledger, tell the user to switch conversations40% warn / 70% stop
Unregistering a roleRefused when the handoff directory has not been written lately (handover_missing)30 minutes

All three append one user message to the request that was going to happen anyway — no extra turn, no extra model call. Each costs 100–200 tokens and saves the tens of thousands that reading a 95 KB ledger in full would cost on the very first request.

A briefing looks like this (only when briefingFiles / briefingScan match real files):

[BUS 开工简报] 本工作区有 handoff/ 协作目录:<workspace>/handoff
开工顺序(先读完再动手;大文件不要整篇读):
1. 全文读:handoff/README.md、handoff/PROTOCOL.md
2. 只读结构与末尾:handoff/a-report.md —— 先 grep -n '^## ' 看标题,再 tail -n 80 看最新回执;
3. 要细节再按标题定位:sed -n 'A,Bp' 文件;不要 cat 整篇,也不要把大段日志贴进对话。
状态规矩:写回 handoff/&lt;你的角色&gt;.md(.md,不是 .txt;含未回报的指令 ref 与产物路径)。不收工不要注销,做完回报后待命即可。

The estimate is derived from the prompt text (ASCII ≈ 3.8 chars/token, CJK ≈ 1.6) plus contextOverheadTokens for the system prompt and tool definitions. It is an estimate within roughly 15%, not the host's exact number.

The estimate is only the fallback. For a registered conversation, the context in a tool result is the same measurement the retirement decision uses (the host's projection buckets plus the compaction count); only when the host cannot be probed does it fall back to the character estimate, flagged estimated: true. Two different numbers would be worse than one imprecise number: bus_list once read 7.6% (the 40,000 constant over an empty message list) while the plugin was already treating the same session as 78%.

contextWindowTokens defaults to 524288, not the declared 1048576: in practice context overflow starts around 430k, so 40% of the declared window would fire after it is already too late.

Token cost

The bus itself is cheap; the turns it triggers are not.

ItemCost
Tool schemas (5 tools)~1400 tokens per request, in every conversation, used or not
Message wrapper~120 tokens per delivered message (header, workspace, time, protocol lines) plus a few dozen for done
A woken turnthe target's whole context, re-sent as input

The third row dominates. Waking a conversation that already holds 30k tokens of history costs 30k input tokens for a 20-character message — and the message stays in that history, so every later turn of that session is slightly more expensive as well.

Since 0.3.0 the plugin remembers items 1 and 3 for you (briefing, budget notice, handover gate); nobody has to watch it.

Ways to spend less, best first:

  1. Never run one line to the end (biggest win): hand it off when it reaches the retirement line, before it starts forgetting and re-reading the same file. But leaving right after a task is pure waste — report, then stand by; the next instruction reuses the same warm context. Leaving early means the next instruction needs a brand-new conversation with a cold history.
  2. Stop pasting logs into the conversation: write them to a file, grep the hits, paste only those lines.
  3. Make done specific — it removes the "one more thing" follow-up that costs a whole context reload.
  4. Reserve urgent for stuck/failed/decision — everything else rides the digest.
  5. wake: false for anything that is not a "start working now" signal.
  6. Use kind="通知" for state sync: it lands on disk and costs nothing.
  7. Never poll. A main conversation calling bus_list every few minutes re-reads the most expensive context and learns nothing.
  8. Pointers, not payloads. Summary + file path in the body; the report stays in the file.
  9. Disable the plugin in profiles that never use it — that removes the ~1400-token schema.

New workspace setup (copy-paste prompt)

The bus does not create directories or write task files. In a new workspace:

  1. The main conversation first creates a coordination directory (the default convention is handoff/; bus state goes in its .bus/ subdirectory) with its normal file tools. Do this first if you want state kept inside the project; otherwise state goes to the DSH user directory.
  2. bus_register(role="main").
  3. Then write task/instruction files and dispatch with bus_send.

The file names in the skeleton below are examples; rename them freely. A numeric prefix such as 40- is only an optional ordering note.

Set up handoff/ in this directory and start working through the session bus.

Step 1 — create the directory and skeleton files (follow this project's existing
handoff/ convention; if there is none, use the skeleton below):
  handoff/README.md
  handoff/instructions.md
  handoff/pending.md
  handoff/task-a.md, task-b.md
  handoff/status-a.md, status-b.md  (current status per worker)
  handoff/report-a.md, report-b.md

Step 2 — register first, then verify:
  bus_register(role="main", note="<date> main")
  bus_list

Step 3 — to dispatch, write the task/instruction file first, then call:
  bus_send(target="api", kind="指令", ref="D-001",
           done="report-a.md is written and npm test passes",
           message="Task file task-a.md is updated. Read it and D-001; when done write report-a.md and bus_send back to main.")

Step 4 — sub-conversations only write their own status/report files;
handoff/.bus/ is the bus state (bindings.json / deliveries.jsonl) — do not edit it.

Create the files, register main, then show me the role tree with bus_list.

Sub-conversation prompt (send this at the start of every new conversation on a line):

You are a new conversation on the api line. First call bus_register(parent="api") to get your number,
then read handoff/api/STATE.md for progress. Only write your own status/report files under handoff/api/.
When done call bus_send(target="main", kind="回报", ref="R-<date>-01", message="Report written; conclusion: …; next: …").
Add urgent: true when stuck, failed, or you need a decision.

Multiple workspaces

  • The bus is scoped by the caller session's cwd. main in workspace A and main in workspace B are two independent buses; role names do not collide.
  • Within one bus, the main and sub conversations must share the same cwd to address each other.
  • State location:
    • workspace has handoff/ → <workspace>/handoff/.bus/
    • otherwise → <DSH_HOME>/handoff-bus/<sanitized-workspace>/
    • The first bus_register creates handoff/, migrating an existing central state into it first, so the move never orphans bindings or counters.
  • Cross-workspace addressing is not implemented. Open an issue if you need it.

Tools

ToolPurpose
bus_register(role?, parent?, replaces?, note?)Register this conversation. parent alone allocates the next free child; a bare line name in role is coerced to parent (coercedFrom). Result carries queue standing
bus_unregister(role?, force?)Leave the table only when archiving (context full) or the user says the work is over — not after each task. Refused while an instruction is unanswered
bus_whoami()Current role + workspace
bus_list()Role tree (parent / depth / children / virtual) with signals (awaiting / msgCount / idleMin / routeTo / suggestHandoff) plus latest
bus_send(target, message, kind?, done?, ref?, urgent?, wake?, preempt?)Deliver to a role or a line; a line name queues to the lowest-numbered usable conversation and the result carries routedFrom / routedTo

kind: 指令 (instruction), 回报 (report), 通知 (notice), 请求 (request). These four values are literal tokens — keep them in Chinese; do not translate them.

bus_whoami / bus_list / bus_send carry context (measured pct / tokens / level / compactions; estimated: true when the host could not be probed); bus_register / bus_whoami / bus_list also carry handoff (directory, entry files, minutes since the last write); bus_send adds targetContext and warns when the target is already past the watermark — archive it instead of piling on more work.

Wake policy when wake is omitted: 指令 / 通知 wake immediately; 回报 goes to the digest without waking; 请求 wakes immediately. An explicit wake always wins.

Configuration

Configuration is read in this order (later wins):

  1. built-in defaults (DEFAULT_CONFIG)
  2. JSON file: $DSH_HANDOFF_BUS_CONFIG, or <DSH_HOME>/handoff-bus/config.json
  3. the plugin row's config in your profile patch (advanced)

Example config.json:

{
  "handoffDir": "handoff",
  "stateDirName": ".bus",
  "maxMessageChars": 4000,
  "maxSendsPerHour": 30,
  "maxPairSendsPer10Min": 8,
  "requireDone": true,
  "routeMaxAttempts": 3,
  "retiredRetentionDays": 7,
  "digestReports": true,
  "digestMinCount": 3,
  "digestMaxWaitSec": 900,
  "handoffAfterMessages": 40,
  "handoffAfterIdleMin": 30,
  "briefing": true,
  "briefingFiles": ["README.md", "PROTOCOL.md"],
  "briefingScan": [],
  "briefingScanLimit": 4,
  "briefingTailLines": 80,
  "contextBudget": true,
  "contextWindowTokens": 524288,
  "contextWarnRatio": 0.4,
  "contextStopRatio": 0.7,
  "contextOverheadTokens": 40000,
  "childWarnRatio": 0.5,
  "childStopRatio": 0.7,
  "mainWarnRatio": 0.5,
  "mainDrainRatio": 0.85,
  "compactionWarn": 1,
  "compactionStop": 2,
  "contextProbeSec": 60,
  "drainSweepSec": 900,
  "drainSweepIdleRounds": 3,
  "drainSweepBackoffSec": 3600,
  "requireHandoverOnLeave": true,
  "handoverWindowMin": 30
}

centralStateRoot is optional (an absolute path); it defaults to <DSH_HOME>/handoff-bus. Restart the client after editing the file. See config.example.json.

KeyDefaultMeaning
handoffDirhandoffCoordination directory name; if present, state goes to <it>/.bus/
stateDirName.busState subdirectory name
centralStateRoot<DSH_HOME>/handoff-busState root when the workspace has no coordination directory
maxMessageChars4000Per-message limit
maxSendsPerHour30Per-conversation hourly limit
maxPairSendsPer10Min8Per role-pair limit, to stop ping-pong
requireDonetruekind="指令" must carry done
routeMaxAttempts3Candidates probed when target is a line name
retiredRetentionDays7How long retired roles are remembered
digestReportstrueBatch plain reports (disable to wake on every report)
digestMinCount3Reports per digest wake-up
digestMaxWaitSec900Longest a report waits before the digest fires
handoffAfterMessages40suggestHandoff threshold: messages exchanged
handoffAfterIdleMin30suggestHandoff threshold: minutes idle
briefingtrueInject the start-up briefing before the first request of a conversation
briefingFiles["README.md","PROTOCOL.md"]Files to read in full, relative to the coordination directory
briefingScan[]Globs read as structure + tail, e.g.

deliveries.jsonl is append-only. Delete it manually if it grows; automatic rotation is planned for a later release.

Error codes

CodeMeaning
no_sessionThe tool call has no associated conversation
no_workspaceThe conversation has no cwd
invalid_role / invalid_target / invalid_message / invalid_kindInvalid argument
caller_not_registeredThis conversation has no role yet
target_not_registeredThe target role or line does not exist in this workspace
line_busyA conversation on this line still owes a 回报; wait, or preempt: true
no_available_sessionEvery conversation on the line is retired (context full), or the line is empty
drainingThis main conversation is winding down: reports only, no new instructions
self_retiredThis conversation is retired (context full): write the ledger, ask the user to archive it
role_not_found / role_virtual / not_ownerbus_unregister: no such role / it is a virtual line node / it belongs to another conversation
self_targetThe target is this conversation
target_unresolvableThe target session cannot be opened (archived/deleted, or owned by subagent routing)
rate_limitedRate limit hit
instruction_pendingbus_unregister only: an instruction to this role is still unanswered
instruction_incompletekind="指令" is missing done
handover_missingThe coordination directory was not written before leaving; write the state, or pass force: true
flush_failedQueued but persistence was not acknowledged
delivery_failedDelivery error
aborted

Security and trust model

  • This is a host plugin with host-process privileges. Read the code before installing.
  • Within one workspace, any conversation can register any role (including impersonating main). Use it only in a trusted, single-user profile.
  • Bus messages are injected as user-role messages and can wake the target. There are rate limits and an audit log, but no signing or encryption.
  • deliveries.jsonl records metadata only (who, when, to whom, message id, length); it does not store the body.
  • No network requests, no telemetry.

Uninstall and cleanup

  • Remove the plugin from the Plugins page and restart.
  • State is not removed automatically: <workspace>/handoff/.bus/ or <DSH_HOME>/handoff-bus/ — delete manually if needed.

Development

npm test

The test uses a fake DSH context and fake agents in a temporary directory; it needs no running Harness and never touches real state.

Design constraint: do not reintroduce @deepseek-ai/* imports (linked installs will fail with ERR_MODULE_NOT_FOUND). Tools are registered through the raw ctx.tools.register contract, and messages are constructed locally in the same shape as createUserMessage.

Repository: https://github.com/cwh-123456/dsh-handoff-bus

License

MIT — see LICENSE. This is an unofficial plugin, not affiliated with DeepSeek.

["4*-*.md"]
briefingScanLimit4How many matching ledgers to name (newest first)
briefingTailLines80Lines in the tail -n suggested by the briefing
contextBudgettrueContext watermark notices
contextWindowTokens524288What counts as 100% (not the declared 1048576)
contextWarnRatio0.4Warn: finish the current task, write the ledger, switch conversations
contextStopRatio0.7Generic watermark for conversations that are not registered on the bus
contextOverheadTokens40000Fixed cost of the system prompt and tool definitions
childWarnRatio / childStopRatio0.5 / 0.7Water level at which a line conversation is advised / retired
mainWarnRatio / mainDrainRatio0.5 / 0.85Water level at which the main conversation is advised / starts draining
compactionWarn / compactionStop1 / 2Compaction count that advises / retires (outranks the level)
contextProbeSec60How long a measured context is reused
drainSweepSec900Idle check interval while the main conversation drains
drainSweepIdleRounds3Quiet rounds before backing off
drainSweepBackoffSec3600Backed-off interval
requireHandoverOnLeavetrueRefuse bus_unregister when the coordination directory is stale
handoverWindowMin30Window for the check above, in minutes
The tool call was cancelled