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 +125 -0
- package/README.md +236 -47
- package/README.zh.md +226 -34
- package/config.example.json +21 -1
- package/index.js +1106 -126
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
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 →
|
|
10
|
-
- ✅
|
|
11
|
-
- ✅
|
|
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
|
-
- ❌
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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`,
|
|
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
|
-
-
|
|
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(
|
|
73
|
-
3. `bus_list` shows the role
|
|
74
|
-
4. Main →
|
|
75
|
-
5.
|
|
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
|
|
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
|
|
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
|
-
|
|
|
86
|
-
|
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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="
|
|
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
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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()` |
|
|
198
|
-
| `bus_send(target, message, kind?, ref?, wake?, force?)` | Deliver to a role
|
|
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 |
|