dsh-handoff-bus 0.1.1 → 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 +291 -0
- package/README.md +326 -54
- package/README.zh.md +276 -43
- package/config.example.json +31 -2
- package/index.js +2017 -152
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,296 @@
|
|
|
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
|
+
|
|
169
|
+
## 0.3.0 — 2026-10-19
|
|
170
|
+
|
|
171
|
+
The previous release made short conversations possible; this one makes them
|
|
172
|
+
**automatic**. Three things that a person had to remember are now enforced by the
|
|
173
|
+
plugin, and all three attack the same number: how many tokens a conversation
|
|
174
|
+
carries into every single request.
|
|
175
|
+
|
|
176
|
+
### Added — start-up briefing
|
|
177
|
+
|
|
178
|
+
Before a conversation's first request, the plugin appends one short message that
|
|
179
|
+
tells it which handoff files to read and how:
|
|
180
|
+
|
|
181
|
+
- `briefingFiles` (default `README.md`, `PROTOCOL.md`) are read in full;
|
|
182
|
+
- `briefingScan` globs are read as **structure + tail** — `grep -n '^## '` for the
|
|
183
|
+
headings, then `tail -n briefingTailLines` for the newest entries;
|
|
184
|
+
- everything else is located by heading and read with `sed -n 'A,Bp'`.
|
|
185
|
+
|
|
186
|
+
Reading a 95 KB ledger in full costs ~30k tokens on the very first request; this
|
|
187
|
+
costs ~200 and is announced once per conversation (`briefing: false` disables it).
|
|
188
|
+
Only fires when the workspace actually has a handoff directory.
|
|
189
|
+
|
|
190
|
+
### Added — context budget
|
|
191
|
+
|
|
192
|
+
Each conversation's prompt is measured before it is sent (ASCII ≈ 3.8 chars per
|
|
193
|
+
token, CJK ≈ 1.6, plus `contextOverheadTokens` for the system prompt and tool
|
|
194
|
+
definitions). At `contextWarnRatio` (default 0.4) the conversation is told:
|
|
195
|
+
|
|
196
|
+
- finish the current task, start nothing new;
|
|
197
|
+
- write the state — including unanswered instruction refs and artefact paths —
|
|
198
|
+
into the handoff directory;
|
|
199
|
+
- tell the user the context is too full and a new conversation should take over,
|
|
200
|
+
then stop.
|
|
201
|
+
|
|
202
|
+
At `contextStopRatio` (default 0.7) the wording becomes an instruction to stop
|
|
203
|
+
after the current step. Both are ordinary user messages appended to the same
|
|
204
|
+
request: **no extra turn, no extra model call.** Each level is announced once and
|
|
205
|
+
re-armed if a compaction brings the estimate back down.
|
|
206
|
+
|
|
207
|
+
`contextWindowTokens` defaults to 524288 rather than the declared 1048576,
|
|
208
|
+
because real overflow happens well before the declared window. Set it to your
|
|
209
|
+
provider's real window if you know it.
|
|
210
|
+
|
|
211
|
+
### Added — handover before leaving
|
|
212
|
+
|
|
213
|
+
`bus_unregister` now refuses to unbind a conversation when the handoff directory
|
|
214
|
+
has not been written for `handoverWindowMin` (default 30) minutes, returning
|
|
215
|
+
`handover_missing`. Leaving is the moment a conversation's context dies; the files
|
|
216
|
+
are all the next shift inherits. `force: true` overrides, and the result reports
|
|
217
|
+
how fresh the handoff is.
|
|
218
|
+
|
|
219
|
+
### Added — budget visible in tool results
|
|
220
|
+
|
|
221
|
+
`bus_register` / `bus_whoami` / `bus_list` / `bus_send` carry a `context` block
|
|
222
|
+
(`pct`, `tokens`, `level`), and `bus_send` also reports `targetContext`. Sending an
|
|
223
|
+
instruction to a conversation that is already at 60% now returns a warning telling
|
|
224
|
+
you to archive it instead of piling more work onto a stale context.
|
|
225
|
+
|
|
226
|
+
### Fixed
|
|
227
|
+
|
|
228
|
+
- "Unanswered instruction" was decided by comparing ISO timestamps with `>=`.
|
|
229
|
+
Several deliveries inside one turn share a millisecond, so a report could be
|
|
230
|
+
treated as acknowledging an instruction that came **after** it, silently
|
|
231
|
+
clearing the pending state (and with it the back-pressure and the takeover
|
|
232
|
+
warning). The delivery log now carries a monotonic `order`, replayed from the
|
|
233
|
+
file, so restarts keep the correct answer. Existing `deliveries.jsonl` files
|
|
234
|
+
replay correctly without migration.
|
|
235
|
+
|
|
236
|
+
## 0.2.0 — 2026-10-19
|
|
237
|
+
|
|
238
|
+
Shorter conversations are the only lever that actually lowers cost: a conversation
|
|
239
|
+
sitting at 400k tokens pays that much again on every request, and one at 20k pays
|
|
240
|
+
one twentieth. This release makes "one temporary conversation per task" cheap to
|
|
241
|
+
run and hard to forget.
|
|
242
|
+
|
|
243
|
+
### Added — three-level roles
|
|
244
|
+
|
|
245
|
+
- `bus_register(parent="rgb")` allocates the next free child (`rgb.1`, `rgb.2`, …).
|
|
246
|
+
Nobody has to remember or pre-plan numbers.
|
|
247
|
+
- Registering a dotted role materialises its parent as a **virtual node** (a name
|
|
248
|
+
with no session attached), so lines never need a separate registration step.
|
|
249
|
+
- `bus_send(target="rgb")` routes to the most recently active live conversation
|
|
250
|
+
under that line and reports `routedFrom` / `routedTo`. Naming `rgb.2` explicitly
|
|
251
|
+
still wins. At most `routeMaxAttempts` (3) candidates are probed.
|
|
252
|
+
- `bus_register(replaces="rgb.2")` hands a line over: the old conversation is
|
|
253
|
+
retired, and the result warns when it still had an unanswered instruction (read
|
|
254
|
+
the ledger, do not redo the work from scratch).
|
|
255
|
+
- New tool `bus_unregister` lets a finished conversation leave the table, so the
|
|
256
|
+
line routes to a fresh one. Refused while an instruction is still awaiting a
|
|
257
|
+
report; `force: true` overrides.
|
|
258
|
+
- `bus_list` now returns the tree with `parent` / `depth` / `children` / `virtual`
|
|
259
|
+
/ `awaiting` / `msgCount` / `idleMin` / `routeTo` / `suggestHandoff`.
|
|
260
|
+
- `bindings.json` upgrades from v1 to v2 on read; existing files keep working.
|
|
261
|
+
|
|
262
|
+
### Added — `done` is required on every 指令
|
|
263
|
+
|
|
264
|
+
`bus_send(kind="指令")` without `done` returns `instruction_incomplete`. An
|
|
265
|
+
instruction that does not say what "finished" means invites a second instruction,
|
|
266
|
+
and a second instruction is a whole extra turn of the target's context — the most
|
|
267
|
+
expensive way to say six words. `done` is echoed in the delivered message, so the
|
|
268
|
+
report can be checked against it. Disable with `requireDone: false`.
|
|
269
|
+
|
|
270
|
+
### Added — report digest and `urgent`
|
|
271
|
+
|
|
272
|
+
A plain `回报` is now injected into the target's history **without waking it**, and
|
|
273
|
+
the target is woken once when `digestMinCount` reports (default 3) or
|
|
274
|
+
`digestMaxWaitSec` (default 900s) have accumulated. The digest message lists the
|
|
275
|
+
reports and says the full text is already above it.
|
|
276
|
+
|
|
277
|
+
- `urgent: true` wakes immediately (stuck / failed / decision needed).
|
|
278
|
+
- `kind="请求"` is urgent by default.
|
|
279
|
+
- An urgent message folds the queued backlog into the same wake-up instead of
|
|
280
|
+
waking twice.
|
|
281
|
+
- `wake: true` / `wake: false` still override everything explicitly.
|
|
282
|
+
- The only timer in the plugin exists while reports are queued, and is `unref`'d.
|
|
283
|
+
Reports are already in the target's history, so a restart loses the wake-up, not
|
|
284
|
+
the message.
|
|
285
|
+
|
|
286
|
+
### Changed
|
|
287
|
+
|
|
288
|
+
- Instruction sequence and back-pressure are counted per **actual** target, so
|
|
289
|
+
`target="rgb"` and `target="rgb.2"` cannot be used to sneak a second instruction
|
|
290
|
+
past the guard.
|
|
291
|
+
- `bus_list` no longer lists a role twice after a session re-registers.
|
|
292
|
+
- Documentation rewritten around the three-level model.
|
|
293
|
+
|
|
3
294
|
## 0.1.1 — 2026-10-19
|
|
4
295
|
|
|
5
296
|
### Fixed — instructions piling up
|