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 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