dsh-handoff-bus 0.3.0 → 0.4.2
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 +166 -0
- package/README.md +112 -29
- package/README.zh.md +72 -31
- package/config.example.json +12 -3
- package/index.js +1018 -133
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,171 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Fixed — tool results and the retire decision read the same number
|
|
6
|
+
|
|
7
|
+
`contextReport` (behind `bus_register` / `bus_whoami` / `bus_list` / `bus_send`)
|
|
8
|
+
counted characters and added the 40,000-token constant, while retirement and
|
|
9
|
+
drain were decided from the host's real projection. A fresh conversation
|
|
10
|
+
therefore read `7.6%` (40000 / 524288) while the plugin could already be treating
|
|
11
|
+
it as 20%, 78% or worse. Showing a second, disagreeing number is worse than
|
|
12
|
+
showing none: the human plans around the figure on screen.
|
|
13
|
+
|
|
14
|
+
`contextReport` is now async and goes through `measureSession` — the same probe,
|
|
15
|
+
same 60-second cache, same `min(declared window, contextWindowTokens)` divisor —
|
|
16
|
+
and derives `level` from `contextTier`, so the reported tier is the tier that
|
|
17
|
+
actually retires the session. The character estimate survives only when the host
|
|
18
|
+
offers nothing to measure, and then the result carries `estimated: true`.
|
|
19
|
+
|
|
20
|
+
`context.tokens` is only meaningful with a projection; when only the compaction
|
|
21
|
+
count can be read, `tokens`/`pct` are reported as 0 with `estimated: true`, and
|
|
22
|
+
the `bus_send` overflow warning uses a phrase that refuses to print "0%".
|
|
23
|
+
|
|
24
|
+
## 0.4.1 — 2026-10-19
|
|
25
|
+
|
|
26
|
+
Four defects found by running 0.4.0 for real. Three of them had nothing to do
|
|
27
|
+
with the queue logic and everything to do with what the text told the model.
|
|
28
|
+
|
|
29
|
+
### Fixed — an instruction no longer tells the target to leave
|
|
30
|
+
|
|
31
|
+
`composeBusText` appended 「任务做完就 bus_unregister,不用等人提醒」 to every
|
|
32
|
+
instruction. Models followed it literally: a conversation that reported at
|
|
33
|
+
12:45:13 was retired at 12:45:14, twice in a row, one second later. The line went
|
|
34
|
+
empty, so the next instruction needed a brand-new conversation and a cold
|
|
35
|
+
context — the exact cost the bus exists to avoid.
|
|
36
|
+
|
|
37
|
+
The rule now says the opposite: **report, then stand by**. Retire only when the
|
|
38
|
+
context is full or the user says the work is over. `bus_unregister`'s own
|
|
39
|
+
description was rewritten for the same reason.
|
|
40
|
+
|
|
41
|
+
### Fixed — `handoff/` is created, and existing state is migrated
|
|
42
|
+
|
|
43
|
+
Nothing ever created the directory. Reports landed in the workspace root as
|
|
44
|
+
`.txt`, and `stateDirFor` quietly fell back to `~/.dsh/handoff-bus/<slug>/`
|
|
45
|
+
where nobody would look for them. Worse, the first person to create the folder
|
|
46
|
+
by hand would have silently orphaned every binding.
|
|
47
|
+
|
|
48
|
+
Registration now creates it, and when a central state directory already exists
|
|
49
|
+
it is copied in before the bus is resolved, so counters and bindings survive.
|
|
50
|
+
Every prompt that names a file now names `handoff/<role>.md`.
|
|
51
|
+
|
|
52
|
+
### Fixed — `role: "a"` no longer turns a line into a conversation
|
|
53
|
+
|
|
54
|
+
`bus_register` accepted a bare line name as `role`, and its parameter text
|
|
55
|
+
actively suggested it. Binding the line itself makes routing short-circuit to
|
|
56
|
+
that session, so the numbered substitutes never receive anything.
|
|
57
|
+
|
|
58
|
+
A bare line name passed as `role` is now coerced to `parent` and reported as
|
|
59
|
+
`coercedFrom`, so the wrong parameter cannot produce the wrong topology.
|
|
60
|
+
|
|
61
|
+
### Added — 「注册 a」 is defined, once, for a fresh conversation
|
|
62
|
+
|
|
63
|
+
An unregistered conversation is told the three-level shape and the three steps:
|
|
64
|
+
`bus_register(parent="a")` → read `handoff/a/` → report. The number is allocated
|
|
65
|
+
by the plugin, so nobody has to remember which one is next.
|
|
66
|
+
|
|
67
|
+
It also states where the newcomer stands: `a.6` behind an idle `a.3` is a
|
|
68
|
+
**substitute**, and tells it to stand by. Stated only — the queue order is
|
|
69
|
+
untouched.
|
|
70
|
+
|
|
71
|
+
The hint is deliberately independent of the start-up briefing, which stays silent
|
|
72
|
+
until the folder has files — precisely the state a fresh workspace is in.
|
|
73
|
+
|
|
74
|
+
## 0.4.0 — 2026-10-19
|
|
75
|
+
|
|
76
|
+
0.3.0 gave every conversation a budget and a briefing. 0.4.0 makes the bus
|
|
77
|
+
**hold the queue itself**: numbering, routing, back-pressure and retirement all
|
|
78
|
+
move from "the model remembers to do it" to "the bus refuses otherwise".
|
|
79
|
+
|
|
80
|
+
### Changed — routing is a queue, not a popularity contest
|
|
81
|
+
|
|
82
|
+
`bus_send(target="rgb")` used to pick the most recently active conversation,
|
|
83
|
+
which meant a pre-registered `rgb.2` and `rgb.3` (activity 0) could never be
|
|
84
|
+
chosen. It now picks the **lowest number** among candidates that are not retired
|
|
85
|
+
and hold no unfinished instruction. Pre-registered conversations become what they
|
|
86
|
+
always were meant to be: a bench, not a race.
|
|
87
|
+
|
|
88
|
+
### Changed — child numbers are monotonic and never reused
|
|
89
|
+
|
|
90
|
+
`rgb.1` archived means the next one is `rgb.4`, never `rgb.1` again. The
|
|
91
|
+
high-water mark is persisted in `bindings.counters` because the `retired` archive
|
|
92
|
+
is pruned after `retiredRetentionDays` (7) — a counter that lived there would
|
|
93
|
+
recycle numbers after a week, silently moving an old instruction's meaning onto a
|
|
94
|
+
different conversation. v1/v2 files derive the counter from their existing roles.
|
|
95
|
+
|
|
96
|
+
### Changed — back-pressure has no time escape
|
|
97
|
+
|
|
98
|
+
`pendingInstructionTtlSec` (600s) freed the line automatically, so a long task
|
|
99
|
+
was never protected. Removed: a line owns **one** instruction until it reports.
|
|
100
|
+
`force` is gone from `bus_send` too — it was the same escape under another name.
|
|
101
|
+
A dead conversation releases the line by leaving the role table, not by waiting.
|
|
102
|
+
|
|
103
|
+
### Added — `line_busy` and `no_available_session`
|
|
104
|
+
|
|
105
|
+
Two distinct refusals instead of one vague one: `line_busy` means the line has
|
|
106
|
+
live conversations but all are working; `no_available_session` means everyone is
|
|
107
|
+
retired, or there is nobody at all.
|
|
108
|
+
|
|
109
|
+
### Added — `preempt` (the only way to take a busy line)
|
|
110
|
+
|
|
111
|
+
`preempt: true` on a 指令 delivers via `inject` into the running turn (no new
|
|
112
|
+
turn), forces the body to open with "stop the current task, save the half-done
|
|
113
|
+
work, then switch", and **cancels** the instruction it displaced. A late report
|
|
114
|
+
for the cancelled ref is recorded but never counts as an acknowledgement — a
|
|
115
|
+
stale task finishing must not free the line for the task that replaced it.
|
|
116
|
+
|
|
117
|
+
### Added — retirement on measured context
|
|
118
|
+
|
|
119
|
+
Compaction count and water level are read from the host for **any** session
|
|
120
|
+
(`sessionQuery.observeSession` for `compaction/end`, the projection cache for
|
|
121
|
+
`contextWindow` and the live sums) instead of being estimated from the current
|
|
122
|
+
message list.
|
|
123
|
+
|
|
124
|
+
| | advise | stop |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| compactions | ≥ 1 | ≥ 2 |
|
|
127
|
+
| water level | > 50% | > 70% |
|
|
128
|
+
|
|
129
|
+
**Compaction count wins outright.** Once a conversation has been compacted, the
|
|
130
|
+
percentage it reports afterwards is meaningless — compaction is what resets it.
|
|
131
|
+
So ≥ 2 compactions stops a conversation whatever the level says, and 1 compaction
|
|
132
|
+
only advises even at 78%.
|
|
133
|
+
|
|
134
|
+
Retirement only marks: `retired: true`, `retiredReason: "context_full"`, and the
|
|
135
|
+
level/compaction snapshot at that moment. The session, its context and its place
|
|
136
|
+
in the tree all stay. **A conversation is only retired when it is idle** — on its
|
|
137
|
+
own report, or when routing finds it free — so a task is never cut in half.
|
|
138
|
+
|
|
139
|
+
The bus does not tell the user "rgb.1 should be archived": it drops that sentence
|
|
140
|
+
into `rgb.1`'s own history (`inject`, no wake) and lets the child say it to its own
|
|
141
|
+
window, with the number to ask for next.
|
|
142
|
+
|
|
143
|
+
### Added — the main conversation drains instead of stopping
|
|
144
|
+
|
|
145
|
+
A main conversation at **≥ 2 compactions or > 85%** (wider than a child, because
|
|
146
|
+
stopping it stops the whole workspace) enters *drain*: it stops issuing
|
|
147
|
+
instructions and keeps receiving reports. Nothing interrupts the lines — they
|
|
148
|
+
finish at their own pace. A sweep (900s, backing off to 3600s after 3 quiet
|
|
149
|
+
rounds) watches for every line to go idle; only then is the user told to archive
|
|
150
|
+
and open a new main. No hard stop: a draining conversation being compacted costs
|
|
151
|
+
nothing, because all it needs to remember is "wait".
|
|
152
|
+
|
|
153
|
+
New error codes `draining` and `self_retired`. A new conversation calling
|
|
154
|
+
`bus_register(role="main")` takes the binding over and resets the state; child
|
|
155
|
+
bindings and numbers are untouched.
|
|
156
|
+
|
|
157
|
+
### Added — departure reminder on every instruction
|
|
158
|
+
|
|
159
|
+
Every 指令 ends with "任务做完就 bus_unregister,不用等人提醒" — one line, so the
|
|
160
|
+
next shift does not depend on the user saying "收工".
|
|
161
|
+
|
|
162
|
+
### Config
|
|
163
|
+
|
|
164
|
+
Removed: `blockSecondInstruction`, `pendingInstructionTtlSec`.
|
|
165
|
+
Added: `childWarnRatio`, `childStopRatio`, `mainWarnRatio`, `mainDrainRatio`,
|
|
166
|
+
`compactionWarn`, `compactionStop`, `contextProbeSec`, `drainSweepSec`,
|
|
167
|
+
`drainSweepIdleRounds`, `drainSweepBackoffSec`.
|
|
168
|
+
|
|
3
169
|
## 0.3.0 — 2026-10-19
|
|
4
170
|
|
|
5
171
|
The previous release made short conversations possible; this one makes them
|
package/README.md
CHANGED
|
@@ -35,9 +35,10 @@ you ──open the main conversation──▶ main (bus_register role="main")
|
|
|
35
35
|
│ 4. bus_send(target="main", kind="回报", ref="R-…")
|
|
36
36
|
▼
|
|
37
37
|
main (woken once per few reports)
|
|
38
|
-
│ 5.
|
|
38
|
+
│ 5. rgb.1 stays and waits; only when its context is full
|
|
39
|
+
│ (or the user is done) does it unregister, and rgb.2 takes over
|
|
39
40
|
▼
|
|
40
|
-
rgb.2 (bus_register parent="rgb"
|
|
41
|
+
rgb.2 (bus_register parent="rgb")
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
Keep the bus message short: summary + file pointers. The authoritative content stays in your files.
|
|
@@ -48,7 +49,7 @@ Keep the bus message short: summary + file pointers. The authoritative content s
|
|
|
48
49
|
- 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.
|
|
49
50
|
- No npm dependencies. The plugin imports only Node builtins.
|
|
50
51
|
- 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
|
-
- **
|
|
52
|
+
- **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.
|
|
52
53
|
- **No session creation or closing**: both are GUI-only actions.
|
|
53
54
|
|
|
54
55
|
## Install
|
|
@@ -109,22 +110,44 @@ Role names are lowercase letters, digits, dots, underscores and hyphens, at most
|
|
|
109
110
|
bus_register(parent="rgb") → role: "rgb.1"
|
|
110
111
|
bus_register(parent="rgb", note="calibration") → role: "rgb.2"
|
|
111
112
|
bus_register(role="main") → the main line
|
|
112
|
-
bus_register(role="rgb") →
|
|
113
|
+
bus_register(role="rgb") → same as parent="rgb": you get rgb.N (see below)
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
All you say in each new sub-conversation:
|
|
116
117
|
|
|
117
|
-
>
|
|
118
|
+
> 注册 rgb
|
|
119
|
+
|
|
120
|
+
The plugin sends that conversation a fixed three-step note on its first request
|
|
121
|
+
(`bus_register(parent="rgb")` → read `handoff/rgb/` → report), so the model never
|
|
122
|
+
needs to know which number is next, nor guess between `role` and `parent`.
|
|
123
|
+
|
|
124
|
+
**A line name never becomes a conversation.** `role="rgb"` used to bind the line
|
|
125
|
+
itself, after which routing short-circuited to it and the numbered conversations
|
|
126
|
+
never received anything. A bare line name passed as `role` is now coerced to
|
|
127
|
+
`parent`, and the result says so via `coercedFrom`.
|
|
128
|
+
|
|
129
|
+
Registration also tells you where you stand: if a smaller-numbered conversation
|
|
130
|
+
is already on the line, you are a **substitute** and should stand by. That is
|
|
131
|
+
stated, not enforced — the queue order is untouched, the lowest number always
|
|
132
|
+
wins.
|
|
118
133
|
|
|
119
134
|
### How a target is resolved
|
|
120
135
|
|
|
121
136
|
1. `target="rgb.2"` → that exact conversation, no guessing.
|
|
122
|
-
2. `target="rgb"` →
|
|
123
|
-
|
|
137
|
+
2. `target="rgb"` → the line **queue**: candidates are the conversations that are
|
|
138
|
+
not retired and hold no unfinished instruction, sorted by number, and the
|
|
139
|
+
**lowest number wins**. Each is opened in turn until one resolves (at most
|
|
140
|
+
`routeMaxAttempts`, default 3).
|
|
141
|
+
3. Nobody usable → `line_busy` (there are conversations, all working) or
|
|
142
|
+
`no_available_session` (all retired, or the line is empty).
|
|
124
143
|
|
|
125
144
|
The result carries `routedFrom` / `routedTo`, so nothing is a black box. `bus_list.routeTo` tells you where it would land before you send.
|
|
126
145
|
|
|
127
|
-
> **
|
|
146
|
+
> **A line owns one instruction at a time.** A bench is not a parallel pool: while
|
|
147
|
+
> `rgb.1` owes a 回报, `rgb.2` does not sneak in — the send is refused, and naming
|
|
148
|
+
> `rgb.2` explicitly does not help either. There is no `force`; the only override
|
|
149
|
+
> is `preempt: true`, which injects "stop and save, then switch" into the running
|
|
150
|
+
> turn and cancels the displaced instruction.
|
|
128
151
|
|
|
129
152
|
### Handing a line over
|
|
130
153
|
|
|
@@ -136,13 +159,43 @@ The old conversation moves to `retired` (kept for `retiredRetentionDays`, defaul
|
|
|
136
159
|
|
|
137
160
|
### When to archive a temporary conversation
|
|
138
161
|
|
|
139
|
-
|
|
162
|
+
The bus now decides this from **measured** context (compaction count first, water
|
|
163
|
+
level second) and marks the conversation `retired` when it is idle:
|
|
164
|
+
|
|
165
|
+
| | advise | stop taking work |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| compactions | ≥ 1 | ≥ 2 |
|
|
168
|
+
| water level | > 50% | > 70% |
|
|
169
|
+
|
|
170
|
+
A retired conversation keeps its context and its place in the tree; routing simply
|
|
171
|
+
skips it. The bus does not lecture the user from the outside — it drops one line
|
|
172
|
+
into the retired conversation's own history, and that conversation tells its own
|
|
173
|
+
window: *"archive me, the next one will be `rgb.4`"*.
|
|
140
174
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
175
|
+
`suggestHandoff` still exists as a softer hint (40 messages, or 30 minutes idle)
|
|
176
|
+
for conversations that are merely stale rather than full. `bus_list` also exposes
|
|
177
|
+
`compactions` and `contextPct` once a conversation has been measured.
|
|
144
178
|
|
|
145
|
-
|
|
179
|
+
### When the main conversation has to go
|
|
180
|
+
|
|
181
|
+
The main conversation fills up too, but its line is wider: **≥ 2 compactions or
|
|
182
|
+
> 85%**. Stopping it stops the whole workspace, so the cost is not the same as
|
|
183
|
+
swapping a temporary conversation.
|
|
184
|
+
|
|
185
|
+
It is never cut short. It **drains**:
|
|
186
|
+
|
|
187
|
+
- it stops issuing instructions (`draining` is returned) and keeps receiving reports;
|
|
188
|
+
- no line is interrupted — they finish at their own pace;
|
|
189
|
+
- a background sweep checks every 900s whether every line is idle, backing off to
|
|
190
|
+
1 hour after 3 quiet rounds, because a long task does not deserve to be poked;
|
|
191
|
+
- the moment everything is idle, it says: write the ledger, archive me, open a new
|
|
192
|
+
main and say "take main".
|
|
193
|
+
|
|
194
|
+
A new main calling `bus_register(role="main")` takes the name over; child bindings
|
|
195
|
+
and numbers are untouched, and their reports land on the new main automatically.
|
|
196
|
+
|
|
197
|
+
Being compacted while draining is harmless — at that point it only has to remember
|
|
198
|
+
one word: wait.
|
|
146
199
|
|
|
147
200
|
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
201
|
|
|
@@ -186,9 +239,12 @@ Every delivery is a separate turn in the target conversation. If `main` sends th
|
|
|
186
239
|
are executed in order — the first one runs against a task file that is already at
|
|
187
240
|
version three. Three consequences:
|
|
188
241
|
|
|
189
|
-
- **A second `指令` is refused** while the
|
|
190
|
-
|
|
191
|
-
|
|
242
|
+
- **A second `指令` is refused** while the line still owes a `回报` (`line_busy`).
|
|
243
|
+
Update the task file, wait for the report, then dispatch again. There is no
|
|
244
|
+
`force` and no timeout: a long task keeps the line. To take the line anyway,
|
|
245
|
+
`preempt: true` — it injects into the running turn, orders "stop and save the
|
|
246
|
+
half-done work", and cancels what it displaced. A late report for the cancelled
|
|
247
|
+
`ref` is recorded but never frees the line.
|
|
192
248
|
- **Each `指令` carries its own sequence** ("本线第 3 条指令") and the footer tells the
|
|
193
249
|
receiver to compare it with `bus_list.latest`. A `seq` below the latest means the
|
|
194
250
|
message was superseded: do the newest one, skip the duplicate work.
|
|
@@ -230,13 +286,21 @@ files):
|
|
|
230
286
|
1. 全文读:_HANDOFF/README.md、_HANDOFF/01-铁律.md
|
|
231
287
|
2. 只读结构与末尾:_HANDOFF/41-回报-B线.md —— 先 grep -n '^## ' 看标题,再 tail -n 80 看最新回执;
|
|
232
288
|
3. 要细节再按标题定位:sed -n 'A,Bp' 文件;不要 cat 整篇,也不要把大段日志贴进对话。
|
|
233
|
-
|
|
289
|
+
状态规矩:写回 _HANDOFF/<你的角色>.md(.md,不是 .txt;含未回报的指令 ref 与产物路径)。不收工不要注销,做完回报后待命即可。
|
|
234
290
|
```
|
|
235
291
|
|
|
236
292
|
The estimate is derived from the prompt text (ASCII ≈ 3.8 chars/token, CJK ≈ 1.6)
|
|
237
293
|
plus `contextOverheadTokens` for the system prompt and tool definitions. It is an
|
|
238
294
|
estimate within roughly 15%, not the host's exact number.
|
|
239
295
|
|
|
296
|
+
**The estimate is only the fallback.** For a registered conversation, the `context`
|
|
297
|
+
in a tool result is the same measurement the retirement decision uses (the host's
|
|
298
|
+
projection buckets plus the compaction count); only when the host cannot be probed
|
|
299
|
+
does it fall back to the character estimate, flagged `estimated: true`. Two
|
|
300
|
+
different numbers would be worse than one imprecise number: `bus_list` once read
|
|
301
|
+
7.6% (the 40,000 constant over an empty message list) while the plugin was already
|
|
302
|
+
treating the same session as 78%.
|
|
303
|
+
|
|
240
304
|
`contextWindowTokens` defaults to 524288, not the declared 1048576: in practice
|
|
241
305
|
`context overflow` starts around 430k, so 40% of the declared window would fire
|
|
242
306
|
after it is already too late.
|
|
@@ -260,7 +324,7 @@ handover gate); nobody has to watch it.
|
|
|
260
324
|
|
|
261
325
|
Ways to spend less, best first:
|
|
262
326
|
|
|
263
|
-
1. **
|
|
327
|
+
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.
|
|
264
328
|
2. **Stop pasting logs into the conversation**: write them to a file, grep the hits, paste only those lines.
|
|
265
329
|
3. **Make `done` specific** — it removes the "one more thing" follow-up that costs a whole context reload.
|
|
266
330
|
4. **Reserve `urgent` for stuck/failed/decision** — everything else rides the digest.
|
|
@@ -323,21 +387,24 @@ Add urgent: true when stuck, failed, or you need a decision.
|
|
|
323
387
|
- State location:
|
|
324
388
|
- workspace has `handoff/` → `<workspace>/handoff/.bus/`
|
|
325
389
|
- otherwise → `<DSH_HOME>/handoff-bus/<sanitized-workspace>/`
|
|
390
|
+
- The first `bus_register` creates `handoff/`, migrating an existing central
|
|
391
|
+
state into it first, so the move never orphans bindings or counters.
|
|
326
392
|
- Cross-workspace addressing is not implemented. Open an issue if you need it.
|
|
327
393
|
|
|
328
394
|
## Tools
|
|
329
395
|
|
|
330
396
|
| Tool | Purpose |
|
|
331
397
|
|---|---|
|
|
332
|
-
| `bus_register(role?, parent?, replaces?, note?)` | Register this conversation. `parent` alone allocates the next free child; `
|
|
333
|
-
| `bus_unregister(role?, force?)` | Leave the table when
|
|
398
|
+
| `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 |
|
|
399
|
+
| `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 |
|
|
334
400
|
| `bus_whoami()` | Current role + workspace |
|
|
335
401
|
| `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?,
|
|
402
|
+
| `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` |
|
|
337
403
|
|
|
338
404
|
`kind`: `指令` (instruction), `回报` (report), `通知` (notice), `请求` (request). These four values are literal tokens — keep them in Chinese; do not translate them.
|
|
339
405
|
|
|
340
|
-
|
|
406
|
+
`bus_whoami` / `bus_list` / `bus_send` carry `context` (measured `pct` / `tokens` / `level` /
|
|
407
|
+
`compactions`; `estimated: true` when the host could not be probed);
|
|
341
408
|
`bus_register` / `bus_whoami` / `bus_list` also carry `handoff` (directory, entry files, minutes
|
|
342
409
|
since the last write); `bus_send` adds `targetContext` and warns when the target is already past
|
|
343
410
|
the watermark — archive it instead of piling on more work.
|
|
@@ -361,8 +428,6 @@ Example `config.json`:
|
|
|
361
428
|
"maxMessageChars": 4000,
|
|
362
429
|
"maxSendsPerHour": 30,
|
|
363
430
|
"maxPairSendsPer10Min": 8,
|
|
364
|
-
"blockSecondInstruction": true,
|
|
365
|
-
"pendingInstructionTtlSec": 600,
|
|
366
431
|
"requireDone": true,
|
|
367
432
|
"routeMaxAttempts": 3,
|
|
368
433
|
"retiredRetentionDays": 7,
|
|
@@ -381,6 +446,16 @@ Example `config.json`:
|
|
|
381
446
|
"contextWarnRatio": 0.4,
|
|
382
447
|
"contextStopRatio": 0.7,
|
|
383
448
|
"contextOverheadTokens": 40000,
|
|
449
|
+
"childWarnRatio": 0.5,
|
|
450
|
+
"childStopRatio": 0.7,
|
|
451
|
+
"mainWarnRatio": 0.5,
|
|
452
|
+
"mainDrainRatio": 0.85,
|
|
453
|
+
"compactionWarn": 1,
|
|
454
|
+
"compactionStop": 2,
|
|
455
|
+
"contextProbeSec": 60,
|
|
456
|
+
"drainSweepSec": 900,
|
|
457
|
+
"drainSweepIdleRounds": 3,
|
|
458
|
+
"drainSweepBackoffSec": 3600,
|
|
384
459
|
"requireHandoverOnLeave": true,
|
|
385
460
|
"handoverWindowMin": 30
|
|
386
461
|
}
|
|
@@ -396,8 +471,6 @@ Example `config.json`:
|
|
|
396
471
|
| `maxMessageChars` | 4000 | Per-message limit |
|
|
397
472
|
| `maxSendsPerHour` | 30 | Per-conversation hourly limit |
|
|
398
473
|
| `maxPairSendsPer10Min` | 8 | Per role-pair limit, to stop ping-pong |
|
|
399
|
-
| `blockSecondInstruction` | `true` | Refuse a `指令` while the previous one to the same target has no `回报` yet |
|
|
400
|
-
| `pendingInstructionTtlSec` | 600 | After this long without a `回报`, the pending instruction stops blocking |
|
|
401
474
|
| `requireDone` | `true` | `kind="指令"` must carry `done` |
|
|
402
475
|
| `routeMaxAttempts` | 3 | Candidates probed when `target` is a line name |
|
|
403
476
|
| `retiredRetentionDays` | 7 | How long retired roles are remembered |
|
|
@@ -414,8 +487,15 @@ Example `config.json`:
|
|
|
414
487
|
| `contextBudget` | `true` | Context watermark notices |
|
|
415
488
|
| `contextWindowTokens` | 524288 | What counts as 100% (not the declared 1048576) |
|
|
416
489
|
| `contextWarnRatio` | 0.4 | Warn: finish the current task, write the ledger, switch conversations |
|
|
417
|
-
| `contextStopRatio` | 0.7 |
|
|
490
|
+
| `contextStopRatio` | 0.7 | Generic watermark for conversations that are not registered on the bus |
|
|
418
491
|
| `contextOverheadTokens` | 40000 | Fixed cost of the system prompt and tool definitions |
|
|
492
|
+
| `childWarnRatio` / `childStopRatio` | 0.5 / 0.7 | Water level at which a line conversation is advised / retired |
|
|
493
|
+
| `mainWarnRatio` / `mainDrainRatio` | 0.5 / 0.85 | Water level at which the main conversation is advised / starts draining |
|
|
494
|
+
| `compactionWarn` / `compactionStop` | 1 / 2 | Compaction count that advises / retires (outranks the level) |
|
|
495
|
+
| `contextProbeSec` | 60 | How long a measured context is reused |
|
|
496
|
+
| `drainSweepSec` | 900 | Idle check interval while the main conversation drains |
|
|
497
|
+
| `drainSweepIdleRounds` | 3 | Quiet rounds before backing off |
|
|
498
|
+
| `drainSweepBackoffSec` | 3600 | Backed-off interval |
|
|
419
499
|
| `requireHandoverOnLeave` | `true` | Refuse `bus_unregister` when the coordination directory is stale |
|
|
420
500
|
| `handoverWindowMin` | 30 | Window for the check above, in minutes |
|
|
421
501
|
|
|
@@ -430,12 +510,15 @@ Example `config.json`:
|
|
|
430
510
|
| `invalid_role` / `invalid_target` / `invalid_message` / `invalid_kind` | Invalid argument |
|
|
431
511
|
| `caller_not_registered` | This conversation has no role yet |
|
|
432
512
|
| `target_not_registered` | The target role or line does not exist in this workspace |
|
|
433
|
-
| `
|
|
513
|
+
| `line_busy` | A conversation on this line still owes a `回报`; wait, or `preempt: true` |
|
|
514
|
+
| `no_available_session` | Every conversation on the line is retired (context full), or the line is empty |
|
|
515
|
+
| `draining` | This main conversation is winding down: reports only, no new instructions |
|
|
516
|
+
| `self_retired` | This conversation is retired (context full): write the ledger, ask the user to archive it |
|
|
434
517
|
| `role_not_found` / `role_virtual` / `not_owner` | `bus_unregister`: no such role / it is a virtual line node / it belongs to another conversation |
|
|
435
518
|
| `self_target` | The target is this conversation |
|
|
436
519
|
| `target_unresolvable` | The target session cannot be opened (archived/deleted, or owned by subagent routing) |
|
|
437
520
|
| `rate_limited` | Rate limit hit |
|
|
438
|
-
| `instruction_pending` |
|
|
521
|
+
| `instruction_pending` | `bus_unregister` only: an instruction to this role is still unanswered |
|
|
439
522
|
| `instruction_incomplete` | `kind="指令"` is missing `done` |
|
|
440
523
|
| `handover_missing` | The coordination directory was not written before leaving; write the state, or pass `force: true` |
|
|
441
524
|
| `flush_failed` | Queued but persistence was not acknowledged |
|