dsh-handoff-bus 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,130 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-19
4
+
5
+ The previous release made short conversations possible; this one makes them
6
+ **automatic**. Three things that a person had to remember are now enforced by the
7
+ plugin, and all three attack the same number: how many tokens a conversation
8
+ carries into every single request.
9
+
10
+ ### Added — start-up briefing
11
+
12
+ Before a conversation's first request, the plugin appends one short message that
13
+ tells it which handoff files to read and how:
14
+
15
+ - `briefingFiles` (default `README.md`, `PROTOCOL.md`) are read in full;
16
+ - `briefingScan` globs are read as **structure + tail** — `grep -n '^## '` for the
17
+ headings, then `tail -n briefingTailLines` for the newest entries;
18
+ - everything else is located by heading and read with `sed -n 'A,Bp'`.
19
+
20
+ Reading a 95 KB ledger in full costs ~30k tokens on the very first request; this
21
+ costs ~200 and is announced once per conversation (`briefing: false` disables it).
22
+ Only fires when the workspace actually has a handoff directory.
23
+
24
+ ### Added — context budget
25
+
26
+ Each conversation's prompt is measured before it is sent (ASCII ≈ 3.8 chars per
27
+ token, CJK ≈ 1.6, plus `contextOverheadTokens` for the system prompt and tool
28
+ definitions). At `contextWarnRatio` (default 0.4) the conversation is told:
29
+
30
+ - finish the current task, start nothing new;
31
+ - write the state — including unanswered instruction refs and artefact paths —
32
+ into the handoff directory;
33
+ - tell the user the context is too full and a new conversation should take over,
34
+ then stop.
35
+
36
+ At `contextStopRatio` (default 0.7) the wording becomes an instruction to stop
37
+ after the current step. Both are ordinary user messages appended to the same
38
+ request: **no extra turn, no extra model call.** Each level is announced once and
39
+ re-armed if a compaction brings the estimate back down.
40
+
41
+ `contextWindowTokens` defaults to 524288 rather than the declared 1048576,
42
+ because real overflow happens well before the declared window. Set it to your
43
+ provider's real window if you know it.
44
+
45
+ ### Added — handover before leaving
46
+
47
+ `bus_unregister` now refuses to unbind a conversation when the handoff directory
48
+ has not been written for `handoverWindowMin` (default 30) minutes, returning
49
+ `handover_missing`. Leaving is the moment a conversation's context dies; the files
50
+ are all the next shift inherits. `force: true` overrides, and the result reports
51
+ how fresh the handoff is.
52
+
53
+ ### Added — budget visible in tool results
54
+
55
+ `bus_register` / `bus_whoami` / `bus_list` / `bus_send` carry a `context` block
56
+ (`pct`, `tokens`, `level`), and `bus_send` also reports `targetContext`. Sending an
57
+ instruction to a conversation that is already at 60% now returns a warning telling
58
+ you to archive it instead of piling more work onto a stale context.
59
+
60
+ ### Fixed
61
+
62
+ - "Unanswered instruction" was decided by comparing ISO timestamps with `>=`.
63
+ Several deliveries inside one turn share a millisecond, so a report could be
64
+ treated as acknowledging an instruction that came **after** it, silently
65
+ clearing the pending state (and with it the back-pressure and the takeover
66
+ warning). The delivery log now carries a monotonic `order`, replayed from the
67
+ file, so restarts keep the correct answer. Existing `deliveries.jsonl` files
68
+ replay correctly without migration.
69
+
70
+ ## 0.2.0 — 2026-10-19
71
+
72
+ Shorter conversations are the only lever that actually lowers cost: a conversation
73
+ sitting at 400k tokens pays that much again on every request, and one at 20k pays
74
+ one twentieth. This release makes "one temporary conversation per task" cheap to
75
+ run and hard to forget.
76
+
77
+ ### Added — three-level roles
78
+
79
+ - `bus_register(parent="rgb")` allocates the next free child (`rgb.1`, `rgb.2`, …).
80
+ Nobody has to remember or pre-plan numbers.
81
+ - Registering a dotted role materialises its parent as a **virtual node** (a name
82
+ with no session attached), so lines never need a separate registration step.
83
+ - `bus_send(target="rgb")` routes to the most recently active live conversation
84
+ under that line and reports `routedFrom` / `routedTo`. Naming `rgb.2` explicitly
85
+ still wins. At most `routeMaxAttempts` (3) candidates are probed.
86
+ - `bus_register(replaces="rgb.2")` hands a line over: the old conversation is
87
+ retired, and the result warns when it still had an unanswered instruction (read
88
+ the ledger, do not redo the work from scratch).
89
+ - New tool `bus_unregister` lets a finished conversation leave the table, so the
90
+ line routes to a fresh one. Refused while an instruction is still awaiting a
91
+ report; `force: true` overrides.
92
+ - `bus_list` now returns the tree with `parent` / `depth` / `children` / `virtual`
93
+ / `awaiting` / `msgCount` / `idleMin` / `routeTo` / `suggestHandoff`.
94
+ - `bindings.json` upgrades from v1 to v2 on read; existing files keep working.
95
+
96
+ ### Added — `done` is required on every 指令
97
+
98
+ `bus_send(kind="指令")` without `done` returns `instruction_incomplete`. An
99
+ instruction that does not say what "finished" means invites a second instruction,
100
+ and a second instruction is a whole extra turn of the target's context — the most
101
+ expensive way to say six words. `done` is echoed in the delivered message, so the
102
+ report can be checked against it. Disable with `requireDone: false`.
103
+
104
+ ### Added — report digest and `urgent`
105
+
106
+ A plain `回报` is now injected into the target's history **without waking it**, and
107
+ the target is woken once when `digestMinCount` reports (default 3) or
108
+ `digestMaxWaitSec` (default 900s) have accumulated. The digest message lists the
109
+ reports and says the full text is already above it.
110
+
111
+ - `urgent: true` wakes immediately (stuck / failed / decision needed).
112
+ - `kind="请求"` is urgent by default.
113
+ - An urgent message folds the queued backlog into the same wake-up instead of
114
+ waking twice.
115
+ - `wake: true` / `wake: false` still override everything explicitly.
116
+ - The only timer in the plugin exists while reports are queued, and is `unref`'d.
117
+ Reports are already in the target's history, so a restart loses the wake-up, not
118
+ the message.
119
+
120
+ ### Changed
121
+
122
+ - Instruction sequence and back-pressure are counted per **actual** target, so
123
+ `target="rgb"` and `target="rgb.2"` cannot be used to sneak a second instruction
124
+ past the guard.
125
+ - `bus_list` no longer lists a role twice after a session re-registers.
126
+ - Documentation rewritten around the three-level model.
127
+
3
128
  ## 0.1.1 — 2026-10-19
4
129
 
5
130
  ### Fixed — instructions piling up
package/README.md CHANGED
@@ -6,14 +6,18 @@ A small host plugin for [DeepSeek Harness](https://www.deepseek.com/harness/) (D
6
6
 
7
7
  ## What it does
8
8
 
9
- - ✅ Main → sub: `bus_send(target="worker-a", kind="指令", ...)`
10
- - ✅ Sub → main: `bus_send(target="main", kind="回报", ...)`
11
- - ✅ Addresses conversations by role name; the target is woken (`wake: true` by default)
9
+ - ✅ Main → line: `bus_send(target="rgb", kind="指令", done="…", ...)` — write only the line name and it lands on whichever conversation under that line is alive
10
+ - ✅ Line → main: `bus_send(target="main", kind="回报", ...)`
11
+ - ✅ Three levels: `main` / line `rgb` / temporary conversations `rgb.1`, `rgb.2`; replacing a conversation never changes what the main conversation writes
12
12
  - ✅ Scoped by session cwd: each workspace gets its own independent bus
13
+ - ✅ 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
13
14
 
14
15
  ## What it does not do
15
16
 
16
- - ❌ No file watching, no timers, no autonomous work when you are away
17
+ - ❌ **It does not create conversations.** You open them in the GUI; the plugin only registers and routes.
18
+ - ❌ **It does not close conversations.** It gives you the signal (`suggestHandoff`); closing is your call.
19
+ - ❌ **It does not compress context.** Compaction is triggered by the host; the plugin can neither read the water level nor change the threshold.
20
+ - ❌ No file watching, no polling, no autonomous work when you are away
17
21
  - ❌ 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
18
22
  - ❌ No cross-workspace addressing (one bus per workspace)
19
23
  - ❌ 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`
@@ -21,16 +25,19 @@ A small host plugin for [DeepSeek Harness](https://www.deepseek.com/harness/) (D
21
25
  ## How it works
22
26
 
23
27
  ```
24
- you ──open the main conversation──▶ main (bus_register main)
25
- │
26
- │ 1. write task files / instruction files
27
- │ 2. bus_send(target="worker-a", kind="指令", ref="D-001", message="…")
28
- ▼
29
- worker-a sub-conversation (bus_register worker-a) ← woken
30
- │ 3. do the work, write the report file
31
- │ 4. bus_send(target="main", kind="回报", ref="R-…")
32
- ▼
33
- main ← woken, reads the report, decides the next step
28
+ you ──open the main conversation──▶ main (bus_register role="main")
29
+ │
30
+ │ 1. write task files / instruction files
31
+ │ 2. bus_send(target="rgb", kind="指令", done="…", message="…")
32
+ ▼
33
+ rgb line ──▶ rgb.1 (bus_register parent="rgb") ← woken
34
+ │ 3. do the work, write the report file
35
+ │ 4. bus_send(target="main", kind="回报", ref="R-…")
36
+ ▼
37
+ main (woken once per few reports)
38
+ │ 5. this task is done: unregister rgb.1, open rgb.2 for the next one
39
+ ▼
40
+ rgb.2 (bus_register parent="rgb" replaces="rgb.1")
34
41
  ```
35
42
 
36
43
  Keep the bus message short: summary + file pointers. The authoritative content stays in your files.
@@ -38,9 +45,11 @@ Keep the bus message short: summary + file pointers. The authoritative content s
38
45
  ## Requirements
39
46
 
40
47
  - 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`).
41
- - Tested on DSH `0.2.0-rc.x` (desktop, Windows). Host services such as `sessionController.resolveAgent`, `agent.followup/inject`, `sessions.flush`, and `session.header.cwd` are internal APIs, not a public contract; a future DSH release may require a patch.
48
+ - 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.
42
49
  - No npm dependencies. The plugin imports only Node builtins.
43
- - v0.1 keeps tool result messages and relayed message bodies in Chinese; error codes are stable English identifiers. Role names are free-form (the examples use Chinese line names).
50
+ - Tool result messages and relayed message bodies are in Chinese; error codes are stable English identifiers. Role names are lowercase ASCII with dots (`main`, `rgb`, `rgb.1`).
51
+ - **No context-length reading**: the plugin does not touch the host's projection API, so `bus_list` reports bus activity (message count, idle minutes), not token water level. Use dsh-context's `/context` for the real number.
52
+ - **No session creation or closing**: both are GUI-only actions.
44
53
 
45
54
  ## Install
46
55
 
@@ -69,21 +78,106 @@ dsh plugin --profile <profile> add dsh-handoff-bus
69
78
  ## Quick start
70
79
 
71
80
  1. Main conversation: `bus_register(role="main")`
72
- 2. Each sub-conversation: `bus_register(role="worker-a")` (any role name)
73
- 3. `bus_list` shows the role table of the current workspace
74
- 4. Main → sub: `bus_send(target="worker-a", kind="指令", ref="D-001", message="…")`
75
- 5. Sub → main: `bus_send(target="main", kind="回报", ref="R-001", message="…")`
81
+ 2. Each sub-conversation: `bus_register(parent="rgb")` → gets `rgb.1`
82
+ 3. `bus_list` shows the role tree and where each line would route
83
+ 4. Main → line: `bus_send(target="rgb", kind="指令", done="…", message="…")`
84
+ 5. Line → main: `bus_send(target="main", kind="回报", ref="R-001", message="…")`
76
85
 
77
- Role names are arbitrary strings you choose; `worker-a` / `worker-b` are only examples.
86
+ Role names are lowercase letters, digits, dots, underscores and hyphens, at most three levels (`main` / `rgb` / `rgb.1` / `rgb.1.worker`).
78
87
 
79
88
  `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.
80
89
 
81
90
  | Situation | Call |
82
91
  |---|---|
83
- | Main gives work to a sub | `bus_send(target="worker-a", kind="指令", ref="D-001", message="…")` |
92
+ | Main gives work to a line | `bus_send(target="rgb", kind="指令", done="pytest tests/test_rgb.py passes", message="…")` |
84
93
  | Sub reports back | `bus_send(target="main", kind="回报", ref="R-…", message="…")` |
85
- | Main asks for status (no work) | `bus_send(target="worker-a", kind="请求", ref="Q-…", message="Status only, do not start work.")` |
86
- | Queue without waking | add `"wake": false` |
94
+ | Stuck, main must act now | same, plus `urgent: true` |
95
+ | Main asks for status (no work) | `bus_send(target="rgb", kind="请求", ref="Q-…", message="Status only, do not start work.")` |
96
+ | Queue without waking | add `wake: false` |
97
+
98
+ ## Three-level roles
99
+
100
+ | Level | Examples | Who maintains it | Rotate? |
101
+ |---|---|---|---|
102
+ | 1 main | `main` | Registered once by you in the main conversation | No. There is exactly one per bus |
103
+ | 2 line | `rgb`, `world`, `scene`, `reward` | Appears automatically as a *virtual node* the first time a child registers under it | **No.** It is only a name; no session is attached |
104
+ | 3 temporary conversation | `rgb.1`, `rgb.2` | Numbered by the plugin when a new conversation registers | **Yes**: one task per conversation, unregister when done, open a new one next time |
105
+
106
+ ### Registering
107
+
108
+ ```
109
+ bus_register(parent="rgb") → role: "rgb.1"
110
+ bus_register(parent="rgb", note="calibration") → role: "rgb.2"
111
+ bus_register(role="main") → the main line
112
+ bus_register(role="rgb") → one conversation owns the line itself (optional)
113
+ ```
114
+
115
+ All you say in each new sub-conversation:
116
+
117
+ > Take work on the rgb line: call `bus_register(parent="rgb")` first, then read `handoff/rgb/STATE.md`.
118
+
119
+ ### How a target is resolved
120
+
121
+ 1. `target="rgb.2"` → that exact conversation, no guessing.
122
+ 2. `target="rgb"` → inside the `rgb` subtree, sorted by most recent traffic, each candidate is opened in turn and **the first one that opens wins** (at most `routeMaxAttempts`, default 3).
123
+ 3. None opens → `no_live_session`, with the hint to open a fresh conversation and `bus_register(parent="rgb")`.
124
+
125
+ The result carries `routedFrom` / `routedTo`, so nothing is a black box. `bus_list.routeTo` tells you where it would land before you send.
126
+
127
+ > **Keep one live conversation per line.** Most-recently-active is not "best suited": if `rgb.2` is busy with task A and you send B to `rgb`, it lands on `rgb.2` behind A. Unregister the old conversation when its task is done, then open a new one — then `target="rgb"` is never ambiguous.
128
+
129
+ ### Handing a line over
130
+
131
+ ```
132
+ bus_register(parent="rgb", replaces="rgb.1")
133
+ ```
134
+
135
+ 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**.
136
+
137
+ ### When to archive a temporary conversation
138
+
139
+ All three at once:
140
+
141
+ 1. `bus_list` shows `awaiting: false` (no unanswered instruction);
142
+ 2. the current task is reported and has no follow-up;
143
+ 3. `suggestHandoff: true` (default: 40 messages exchanged, or 30 minutes idle).
144
+
145
+ `suggestHandoff` is a **heuristic, not a context gauge**. `msgCount` only counts bus messages; the context a conversation accumulates by reading files and running commands is invisible to the bus — use dsh-context's `/context` for the real number.
146
+
147
+ 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.
148
+
149
+ ## Instructions must carry an acceptance test
150
+
151
+ `bus_send(kind="指令")` without `done` is refused (`instruction_incomplete`):
152
+
153
+ ```
154
+ bus_send(target="rgb", kind="指令",
155
+ message="Wire the RGB camera calibration into the data loader",
156
+ done="calibration file is read; all three cameras align on the same frame id; pytest tests/test_rgb.py passes")
157
+ ```
158
+
159
+ `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.
160
+
161
+ Disable with `requireDone: false`.
162
+
163
+ ## Report digest and `urgent`
164
+
165
+ Not every report deserves a wake-up — the main conversation usually holds the longest context, so its wake-ups are the most expensive.
166
+
167
+ | Case | Default |
168
+ |---|---|
169
+ | `kind="回报"` without `urgent` | **No 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 |
170
+ | `kind="回报"` + `urgent: true` | Wakes immediately |
171
+ | `kind="请求"` | Urgent by default (a request is a decision ask) |
172
+ | `wake: true` / `wake: false` | Explicit override, highest priority |
173
+
174
+ Only three things deserve `urgent`: **stuck**, **failed and needs a new approach**, **needs a decision**. "Done", "50%", "here is the artifact path" do not.
175
+
176
+ 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.
177
+
178
+ 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.
179
+
180
+ Disable with `digestReports: false` (back to waking on every report).
87
181
 
88
182
  ## Delivering without piling up
89
183
 
@@ -100,6 +194,8 @@ version three. Three consequences:
100
194
  message was superseded: do the newest one, skip the duplicate work.
101
195
  - **The counter and the pending state are rebuilt from `deliveries.jsonl`**, so a client
102
196
  restart neither resets the numbering nor forgets an unanswered instruction.
197
+ - **Numbering follows the actual target**, so `target="rgb"` and `target="rgb.2"` landing
198
+ on the same conversation share one sequence and cannot be used to bypass the guard.
103
199
 
104
200
  A sub-conversation prompt that makes this reliable:
105
201
 
@@ -109,30 +205,70 @@ A sub-conversation prompt that makes this reliable:
109
205
  只做最新那条,不要重复劳动,并在回报里说明跳过了哪一条。
110
206
  ```
111
207
 
208
+ ## Context budget and start-up briefing
209
+
210
+ The longer a conversation lives, the more every request costs. The three things
211
+ below are what a person would otherwise have to remember; the plugin enforces them.
212
+
213
+ | When | What happens | Default |
214
+ |---|---|---|
215
+ | Before a conversation's first request | A **briefing** is injected: which handoff files to read, and how to read the big ones | `briefing: true` |
216
+ | Context crosses the watermark | A **budget** notice: finish the current task, write the ledger, tell the user to switch conversations | 40% warn / 70% stop |
217
+ | Unregistering a role | Refused when the handoff directory has not been written lately (`handover_missing`) | 30 minutes |
218
+
219
+ All three **append one user message to the request that was going to happen
220
+ anyway** — no extra turn, no extra model call. Each costs 100–200 tokens and saves
221
+ the tens of thousands that reading a 95 KB ledger in full would cost on the very
222
+ first request.
223
+
224
+ A briefing looks like this (only when `briefingFiles` / `briefingScan` match real
225
+ files):
226
+
227
+ ```text
228
+ [BUS 开工简报] 本工作区有 _HANDOFF/ 协作目录:D:\project\_HANDOFF
229
+ 开工顺序(先读完再动手;大文件不要整篇读):
230
+ 1. 全文读:_HANDOFF/README.md、_HANDOFF/01-铁律.md
231
+ 2. 只读结构与末尾:_HANDOFF/41-回报-B线.md —— 先 grep -n '^## ' 看标题,再 tail -n 80 看最新回执;
232
+ 3. 要细节再按标题定位:sed -n 'A,Bp' 文件;不要 cat 整篇,也不要把大段日志贴进对话。
233
+ 收工规矩:把状态写回 _HANDOFF/(含未回报的指令 ref 与产物路径),否则 bus_unregister 会被拒。
234
+ ```
235
+
236
+ The estimate is derived from the prompt text (ASCII ≈ 3.8 chars/token, CJK ≈ 1.6)
237
+ plus `contextOverheadTokens` for the system prompt and tool definitions. It is an
238
+ estimate within roughly 15%, not the host's exact number.
239
+
240
+ `contextWindowTokens` defaults to 524288, not the declared 1048576: in practice
241
+ `context overflow` starts around 430k, so 40% of the declared window would fire
242
+ after it is already too late.
243
+
112
244
  ## Token cost
113
245
 
114
246
  The bus itself is cheap; the turns it triggers are not.
115
247
 
116
248
  | Item | Cost |
117
249
  |---|---|
118
- | Tool schemas (4 tools) | ~700 tokens per request, in every conversation, used or not |
119
- | Message wrapper | ~120 tokens per delivered message (header, workspace, time, protocol lines) |
250
+ | Tool schemas (5 tools) | ~1400 tokens per request, in every conversation, used or not |
251
+ | Message wrapper | ~120 tokens per delivered message (header, workspace, time, protocol lines) plus a few dozen for `done` |
120
252
  | **A woken turn** | **the target's whole context, re-sent as input** |
121
253
 
122
254
  The third row dominates. Waking a conversation that already holds 30k tokens of history
123
255
  costs 30k input tokens for a 20-character message — and the message stays in that
124
256
  history, so every later turn of that session is slightly more expensive as well.
125
257
 
126
- Ways to spend less:
258
+ Since 0.3.0 the plugin remembers items 1 and 3 for you (briefing, budget notice,
259
+ handover gate); nobody has to watch it.
127
260
 
128
- 1. **Batch.** Update the task file, then send one `指令`, not four.
129
- 2. **`wake: false`** for anything that is not a "start working now" signal: it lands in
130
- the inbox and costs nothing until the target runs on its own.
131
- 3. **Keep sub-conversation sessions short.** Wake cost = that session's context length,
132
- so a line that has been running for hours is the expensive one. Finish a milestone,
133
- open a fresh conversation, `bus_register` again.
134
- 4. **Pointers, not payloads.** Summary + file path in the body; the report stays in the file.
135
- 5. **Disable the plugin in profiles that never use it** — that removes the ~700-token schema.
261
+ Ways to spend less, best first:
262
+
263
+ 1. **Shorten conversation life** (biggest win): one task per conversation, `bus_unregister` when done, open a fresh one for the next task. Going from 400k to 20k makes the same work an order of magnitude cheaper.
264
+ 2. **Stop pasting logs into the conversation**: write them to a file, grep the hits, paste only those lines.
265
+ 3. **Make `done` specific** — it removes the "one more thing" follow-up that costs a whole context reload.
266
+ 4. **Reserve `urgent` for stuck/failed/decision** — everything else rides the digest.
267
+ 5. **`wake: false`** for anything that is not a "start working now" signal.
268
+ 6. **Use `kind="通知"` for state sync**: it lands on disk and costs nothing.
269
+ 7. **Never poll.** A main conversation calling `bus_list` every few minutes re-reads the most expensive context and learns nothing.
270
+ 8. **Pointers, not payloads.** Summary + file path in the body; the report stays in the file.
271
+ 9. **Disable the plugin in profiles that never use it** — that removes the ~1400-token schema.
136
272
 
137
273
  ## New workspace setup (copy-paste prompt)
138
274
 
@@ -161,22 +297,23 @@ Step 2 — register first, then verify:
161
297
  bus_list
162
298
 
163
299
  Step 3 — to dispatch, write the task/instruction file first, then call:
164
- bus_send(target="worker-a", kind="指令", ref="D-001",
300
+ bus_send(target="rgb", kind="指令", ref="D-001",
301
+ done="report-a.md is written and pytest tests/test_rgb.py passes",
165
302
  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.")
166
303
 
167
304
  Step 4 — sub-conversations only write their own status/report files;
168
305
  handoff/.bus/ is the bus state (bindings.json / deliveries.jsonl) — do not edit it.
169
306
 
170
- Create the files, register main, then show me the role table with bus_list.
307
+ Create the files, register main, then show me the role tree with bus_list.
171
308
  ```
172
309
 
173
- Sub-conversation prompt:
310
+ Sub-conversation prompt (send this at the start of every new conversation on a line):
174
311
 
175
312
  ```text
176
- Read <this directory>/handoff/task-a.md and start.
177
- First call bus_register(role="worker-a").
178
- Only write status-a.md and report-a.md; when done call
179
- bus_send(target="main", kind="回报", ref="R-<date>-01", message="Report written to report-a.md; conclusion: …; next: …")
313
+ You are a new conversation on the rgb line. First call bus_register(parent="rgb") to get your number,
314
+ then read handoff/rgb/STATE.md for progress. Only write your own status/report files under handoff/rgb/.
315
+ When done call bus_send(target="main", kind="回报", ref="R-<date>-01", message="Report written; conclusion: …; next: …").
316
+ Add urgent: true when stuck, failed, or you need a decision.
180
317
  ```
181
318
 
182
319
  ## Multiple workspaces
@@ -192,13 +329,21 @@ bus_send(target="main", kind="回报", ref="R-<date>-01", message="Report writte
192
329
 
193
330
  | Tool | Purpose |
194
331
  |---|---|
195
- | `bus_register(role, note?)` | Register this conversation as a role **in its workspace** |
332
+ | `bus_register(role?, parent?, replaces?, note?)` | Register this conversation. `parent` alone allocates the next free child; `replaces` takes a line over from an older conversation |
333
+ | `bus_unregister(role?, force?)` | Leave the table when the task is done, so the line routes to a fresh conversation. Refused while an instruction is unanswered |
196
334
  | `bus_whoami()` | Current role + workspace |
197
- | `bus_list()` | All roles **in this workspace** → sessionId / bound time, plus `latest` (newest `指令` per pair with `seq` / `awaitingReport`) |
198
- | `bus_send(target, message, kind?, ref?, wake?, force?)` | Deliver to a role in this workspace; `wake=true` wakes it, `force=true` overrides instruction back-pressure |
335
+ | `bus_list()` | Role tree (`parent` / `depth` / `children` / `virtual`) with signals (`awaiting` / `msgCount` / `idleMin` / `routeTo` / `suggestHandoff`) plus `latest` |
336
+ | `bus_send(target, message, kind?, done?, ref?, urgent?, wake?, force?)` | Deliver to a role or a line; a line name routes automatically and the result carries `routedFrom` / `routedTo` |
199
337
 
200
338
  `kind`: `指令` (instruction), `回报` (report), `通知` (notice), `请求` (request). These four values are literal tokens — keep them in Chinese; do not translate them.
201
339
 
340
+ Every result except `bus_unregister` carries `context` (estimated `pct` / `tokens` / `level`);
341
+ `bus_register` / `bus_whoami` / `bus_list` also carry `handoff` (directory, entry files, minutes
342
+ since the last write); `bus_send` adds `targetContext` and warns when the target is already past
343
+ the watermark — archive it instead of piling on more work.
344
+
345
+ Wake policy when `wake` is omitted: `指令` / `通知` wake immediately; `回报` goes to the digest without waking; `请求` wakes immediately. An explicit `wake` always wins.
346
+
202
347
  ## Configuration
203
348
 
204
349
  Configuration is read in this order (later wins):
@@ -217,7 +362,27 @@ Example `config.json`:
217
362
  "maxSendsPerHour": 30,
218
363
  "maxPairSendsPer10Min": 8,
219
364
  "blockSecondInstruction": true,
220
- "pendingInstructionTtlSec": 600
365
+ "pendingInstructionTtlSec": 600,
366
+ "requireDone": true,
367
+ "routeMaxAttempts": 3,
368
+ "retiredRetentionDays": 7,
369
+ "digestReports": true,
370
+ "digestMinCount": 3,
371
+ "digestMaxWaitSec": 900,
372
+ "handoffAfterMessages": 40,
373
+ "handoffAfterIdleMin": 30,
374
+ "briefing": true,
375
+ "briefingFiles": ["README.md", "PROTOCOL.md"],
376
+ "briefingScan": [],
377
+ "briefingScanLimit": 4,
378
+ "briefingTailLines": 80,
379
+ "contextBudget": true,
380
+ "contextWindowTokens": 524288,
381
+ "contextWarnRatio": 0.4,
382
+ "contextStopRatio": 0.7,
383
+ "contextOverheadTokens": 40000,
384
+ "requireHandoverOnLeave": true,
385
+ "handoverWindowMin": 30
221
386
  }
222
387
  ```
223
388
 
@@ -233,6 +398,26 @@ Example `config.json`:
233
398
  | `maxPairSendsPer10Min` | 8 | Per role-pair limit, to stop ping-pong |
234
399
  | `blockSecondInstruction` | `true` | Refuse a `指令` while the previous one to the same target has no `回报` yet |
235
400
  | `pendingInstructionTtlSec` | 600 | After this long without a `回报`, the pending instruction stops blocking |
401
+ | `requireDone` | `true` | `kind="指令"` must carry `done` |
402
+ | `routeMaxAttempts` | 3 | Candidates probed when `target` is a line name |
403
+ | `retiredRetentionDays` | 7 | How long retired roles are remembered |
404
+ | `digestReports` | `true` | Batch plain reports (disable to wake on every report) |
405
+ | `digestMinCount` | 3 | Reports per digest wake-up |
406
+ | `digestMaxWaitSec` | 900 | Longest a report waits before the digest fires |
407
+ | `handoffAfterMessages` | 40 | `suggestHandoff` threshold: messages exchanged |
408
+ | `handoffAfterIdleMin` | 30 | `suggestHandoff` threshold: minutes idle |
409
+ | `briefing` | `true` | Inject the start-up briefing before the first request of a conversation |
410
+ | `briefingFiles` | `["README.md","PROTOCOL.md"]` | Files to read in full, relative to the coordination directory |
411
+ | `briefingScan` | `[]` | Globs read as structure + tail, e.g. `["4*-*.md"]` |
412
+ | `briefingScanLimit` | 4 | How many matching ledgers to name (newest first) |
413
+ | `briefingTailLines` | 80 | Lines in the `tail -n` suggested by the briefing |
414
+ | `contextBudget` | `true` | Context watermark notices |
415
+ | `contextWindowTokens` | 524288 | What counts as 100% (not the declared 1048576) |
416
+ | `contextWarnRatio` | 0.4 | Warn: finish the current task, write the ledger, switch conversations |
417
+ | `contextStopRatio` | 0.7 | Demand an immediate wrap-up |
418
+ | `contextOverheadTokens` | 40000 | Fixed cost of the system prompt and tool definitions |
419
+ | `requireHandoverOnLeave` | `true` | Refuse `bus_unregister` when the coordination directory is stale |
420
+ | `handoverWindowMin` | 30 | Window for the check above, in minutes |
236
421
 
237
422
  `deliveries.jsonl` is append-only. Delete it manually if it grows; automatic rotation is planned for a later release.
238
423
 
@@ -244,11 +429,15 @@ Example `config.json`:
244
429
  | `no_workspace` | The conversation has no cwd |
245
430
  | `invalid_role` / `invalid_target` / `invalid_message` / `invalid_kind` | Invalid argument |
246
431
  | `caller_not_registered` | This conversation has no role yet |
247
- | `target_not_registered` | The target role does not exist in this workspace |
432
+ | `target_not_registered` | The target role or line does not exist in this workspace |
433
+ | `no_live_session` | The line has no live conversation left; open one and register it |
434
+ | `role_not_found` / `role_virtual` / `not_owner` | `bus_unregister`: no such role / it is a virtual line node / it belongs to another conversation |
248
435
  | `self_target` | The target is this conversation |
249
436
  | `target_unresolvable` | The target session cannot be opened (archived/deleted, or owned by subagent routing) |
250
437
  | `rate_limited` | Rate limit hit |
251
438
  | `instruction_pending` | A `指令` to this target is still unanswered; wait for the `回报`, or pass `force: true` |
439
+ | `instruction_incomplete` | `kind="指令"` is missing `done` |
440
+ | `handover_missing` | The coordination directory was not written before leaving; write the state, or pass `force: true` |
252
441
  | `flush_failed` | Queued but persistence was not acknowledged |
253
442
  | `delivery_failed` | Delivery error |
254
443
  | `aborted` | The tool call was cancelled |