dsh-turn-recovery 0.2.0 → 0.2.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 +334 -231
- package/README.md +28 -15
- package/lib/index.js +369 -63
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,231 +1,334 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
本文件记录 dsh-turn-recovery 的所有重要变更,格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
4
|
-
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
5
|
-
|
|
6
|
-
> **改名说明(0.2.0)**:本插件原名 `dsh-empty-turn-guard`,因与另外两个同名项目(`FYTXY/dsh-empty-turn-guard`、
|
|
7
|
-
> `@nlflora/dsh-empty-turn-guard`)冲突,自 0.2.0 起更名为 `dsh-turn-recovery`。0.1.x 及更早的条目保留旧名原文,
|
|
8
|
-
> 其提到的 `plugin:empty-turn-guard`、`empty-turn-guard.log` 等标识在 0.2.0 起分别改为 `plugin:turn-recovery`、`turn-recovery.log`。
|
|
9
|
-
|
|
10
|
-
## [0.2.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
###
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
121
|
-
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
###
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
`
|
|
178
|
-
`
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
###
|
|
192
|
-
|
|
193
|
-
- `
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- `
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本文件记录 dsh-turn-recovery 的所有重要变更,格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
4
|
+
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
5
|
+
|
|
6
|
+
> **改名说明(0.2.0)**:本插件原名 `dsh-empty-turn-guard`,因与另外两个同名项目(`FYTXY/dsh-empty-turn-guard`、
|
|
7
|
+
> `@nlflora/dsh-empty-turn-guard`)冲突,自 0.2.0 起更名为 `dsh-turn-recovery`。0.1.x 及更早的条目保留旧名原文,
|
|
8
|
+
> 其提到的 `plugin:empty-turn-guard`、`empty-turn-guard.log` 等标识在 0.2.0 起分别改为 `plugin:turn-recovery`、`turn-recovery.log`。
|
|
9
|
+
|
|
10
|
+
## [0.2.2] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
**代码审查修复 + 可验证性补强**。针对 0.2.1 的复审(宿主 `0.2.2-alpha.3` 源码取证、
|
|
13
|
+
`~/.dsh/logs/turn-recovery.log` 28 次激活 0 次 `nudge DELIVERED` 的实证)逐项修复,
|
|
14
|
+
不含行为方向的改变,但对「补写到底有没有发生」的日志与状态记账做了实质纠正。
|
|
15
|
+
|
|
16
|
+
### 新增
|
|
17
|
+
|
|
18
|
+
- **`dryRun` 配置项**:判定链、限流、日志全部照走,唯独不把补写交给宿主。
|
|
19
|
+
这是给「生产环境从未观测到真实补写」准备的验证通道 —— 在真机上把 `dryRun: true`,
|
|
20
|
+
等一次空回合,日志里出现 `nudge DRY-RUN: would deliver` 就说明整条判定链在生产可用,
|
|
21
|
+
而且不会因此多跑一轮模型、不占小时预算。
|
|
22
|
+
|
|
23
|
+
### 修复
|
|
24
|
+
|
|
25
|
+
- **`guardSentForTurn` 先置位、后调度**(P3-1):`scheduleNudge` 可能在内部早退
|
|
26
|
+
(已有在途补写 / 宿主队列里已有一笔 / 干跑),旧版在此之前就把回合记成「已补过」,
|
|
27
|
+
日志写 `already nudged` 而实际一笔没发,同一回合再触发一次 `turn/end` 也就永久放弃。
|
|
28
|
+
现在 `scheduleNudge` 返回 `scheduled` / `dry-run` / `skipped:<原因>`,只有真的接了活才记账,
|
|
29
|
+
并额外打一行 `nudge outcome turn=N: <结果>`。字段同时更名为 `nudgeHandledTurn`(语义更准)。
|
|
30
|
+
- **`turn/end` 提前清空 `pendingTurn`**(P3-2):那道「回合已前进就丢弃定时器」的检查靠它做判据,
|
|
31
|
+
而 `turn/end` 在调度之后又把它清零,于是一个没有 `turn/start` 的第二个 `turn/end` 之后
|
|
32
|
+
检查恒不生效,补写可能插进下一个回合。现在 `turn/end` 不再碰它(更名 `scheduledTurn`),
|
|
33
|
+
清理职责只落在定时器自身与 `clearTimer` 上。
|
|
34
|
+
- **认领超时可重排第二笔**(P3-3):120s 超时后 `nudgeMessageId` 被清掉并允许重排,
|
|
35
|
+
但宿主仍可能认领那笔「已超时」的消息,旧版会把它判成 `NOT ours`,导致后面的空回合
|
|
36
|
+
又被补一笔。现在保留最近 8 条投递 id(`guardIds`),晚到的认领照样算作补写回合
|
|
37
|
+
(日志 `guard message claimed (LATE, from guardIds)`)。同时把「清在途标记」挪到
|
|
38
|
+
宿主队列去重之后,避免清掉标记后重复排期。
|
|
39
|
+
- **`budgetHistory` 只增不减**(P2-2):跨重启限流的内存 map 生命周期等于进程,旧版只在
|
|
40
|
+
预算文件 >1 MB 时重建,重建后 map 里仍留着过期条目。现在重建时按一小时窗口同时修剪 map,
|
|
41
|
+
并每 256 次投递做一次纯内存修剪(清过期条目与空会话)。
|
|
42
|
+
- **配置写错无人报警**(P3-4):宿主不校验插件配置(本包不导出 `Config` schema,原因见 README),
|
|
43
|
+
`maxNudgesPerHr`、`minVisibleChars: "abc"` 这类手滑会被静默兜底成默认值,用户以为改生效了。
|
|
44
|
+
现在加载期检查未知键、数字/布尔/字符串类型、`sessionScope` 取值、`foregroundKinds` 形态,
|
|
45
|
+
逐条写 `config issue: ...` 与汇总行。只报告、不抛错 —— 一个笔误不该让整个插件拒绝加载。
|
|
46
|
+
- **日志 `tag()` 对 `worker-*` 只截前 8 位**(P3-6):前缀剥离正则补上 `worker-`,
|
|
47
|
+
多 worker 会话不再全挤成 `[worker-a]`。
|
|
48
|
+
|
|
49
|
+
### 说明
|
|
50
|
+
|
|
51
|
+
- **`source.kind = 'plugin:turn-recovery'` 不在宿主 `MessageSourceMap` 的类型联合内**(P3-5):
|
|
52
|
+
宿主运行时只校验 `source.kind` 是非空字符串(`dsh-session` 的 `assertMessageEventShape`),
|
|
53
|
+
类型联合的缺项只影响本仓库以外的 TS 读者,前端对未知 kind 有 `default` 分支。
|
|
54
|
+
保持现状并写进 README「已知限制」,不改协议用法。
|
|
55
|
+
- 回归用例 43 → 49:新增 P3-1(早退不记账)、P3-2(回合前进丢弃)、`dryRun`、
|
|
56
|
+
P3-3(晚到认领)、配置写错告警、`worker-` tag。
|
|
57
|
+
|
|
58
|
+
## [0.2.1] - 2026-11-03
|
|
59
|
+
|
|
60
|
+
**适配性修复**:针对 DSH `0.2.2-alpha.3` 实测(宿主 `dsh-session` / `dsh-agent-loop` /
|
|
61
|
+
`dsh-system-prompt` / `dsh-app-boot` 源码取证 + 本机 37 个会话日志复核)。不含新功能,
|
|
62
|
+
但有三处行为变化,升级前请读。
|
|
63
|
+
|
|
64
|
+
### 修复
|
|
65
|
+
|
|
66
|
+
- **会话范围改按 `SessionHeader` 判定**(原 `lib/index.js` 用 `/^session-/` 猜 id 前缀):
|
|
67
|
+
只有 `header.origin === 'subagent'` 或 `header.delegationDepth > 0` 才算子代理会话。
|
|
68
|
+
0.2.0 把 `orchestrator-*` 这类顶层会话全部当成子代理过滤掉,实测日志里有 19 条
|
|
69
|
+
`skip scope: non-root session orchestrator` —— 通过路由器(orchestrator 会话)跑的
|
|
70
|
+
主对话从来没被保护过。宿主没有 header 时(老宿主 / 测试桩)退回 id 形态判定。
|
|
71
|
+
- **取消策略由「后台黑名单」改为「前台白名单」**:默认只有 `source.kind === 'user'`
|
|
72
|
+
与 `'agent-message'` 会取消待发补写,且 `source.form === 'notice'` 永远不取消。
|
|
73
|
+
0.2.0 的黑名单没收录宿主后来新增的 `tool-jobs` / `action-cards` / `plan-mode` /
|
|
74
|
+
`tool-goal`(都是 `form: "notice"`)与 `goal` / `schedule` 自动化来源,它们会让
|
|
75
|
+
已排好的补写被静默取消。未知来源现在一律按后台处理,漏取消由 `turn/start`
|
|
76
|
+
分支兜底;自定义桥接可继续用 `foregroundKinds` 加回取消语义。
|
|
77
|
+
- **prompt 段落注册失败改为 fail-closed**:宿主对同名 section 重复注册会直接 throw
|
|
78
|
+
(同一插件被两个 bundle 各装一次就会这样)。旧版 catch 之后仍打印 `guard armed`,
|
|
79
|
+
于是「222 字符收尾指令其实没进 prompt」在日志里看不出来,且两个实例会各发一条补写。
|
|
80
|
+
现在记 `guard NOT armed (fail-closed: ...)` 并整体不启用。
|
|
81
|
+
- **投放前查宿主队列去重**:`nudgeMessageId` 是进程内记忆,重启 / 热重载 / 双实例时为空。
|
|
82
|
+
投递前检查 `agent.inbox.nextTurn` 里是否已有 `plugin:turn-recovery` 来源的待认领消息,
|
|
83
|
+
有则跳过,避免同一个空回合被连投两笔。
|
|
84
|
+
- **小时预算跨重启生效**:投递时刻追加到 `$DSH_HOME/state/turn-recovery/nudges.log`,
|
|
85
|
+
进程启动后首次见到某会话时回读最近一小时。旧版的 `nudgeTimes` 只在内存里,重启即清零,
|
|
86
|
+
同一会话每次重启都能重新领满额度。
|
|
87
|
+
- **`engines.dsh` 上界收紧为 `<0.3.0-0`**:宿主用 `semver` 的 `includePrerelease: true`
|
|
88
|
+
判定门槛,`<0.3.0` 会放行 `0.3.0-alpha.1` / `0.3.0-beta.2`(实测为 true)。`-0` 是显式的
|
|
89
|
+
预发布截断。`scripts/check-metadata.mjs` 新增规则强制上界必须写成 `x.y.z-0`。
|
|
90
|
+
- **日志可读性**:`guard armed` 现在带 `instruction=registered|disabled`;`skip scope`
|
|
91
|
+
带上判定依据(`header says subagent` / `id is not a root shape`);`tag()` 同时剥掉
|
|
92
|
+
`orchestrator-` 前缀(否则所有 orchestrator 会话在日志里都显示成 `[orchestr]`)。
|
|
93
|
+
- **日志行内的换行被压平**:会话 id、错误文本里带 `\n` 时可以伪造出一行看似插件自己
|
|
94
|
+
写的记录(例如假的 `guard armed` 或 `turn/end`),现在一律替换为空格。
|
|
95
|
+
|
|
96
|
+
### 新增配置
|
|
97
|
+
|
|
98
|
+
- `foregroundKinds: string[]`:追加「会取消待发补写」的来源 `kind`(默认 `user` / `agent-message`)。
|
|
99
|
+
- `budgetFile: string`:补写预算的持久化路径(默认 `$DSH_HOME/state/turn-recovery/nudges.log`)。
|
|
100
|
+
|
|
101
|
+
### 已知边界(不变,仅补文档)
|
|
102
|
+
|
|
103
|
+
- 可见字符按**回合累计**而非末条消息计:先写结论、后被工具调用打断的回合不算空。
|
|
104
|
+
- 非 `completed` 回合(`aborted` / `interrupted` / `error` / `max-tokens` / `blocked`)一律不补写。
|
|
105
|
+
- `minVisibleChars` 默认 1 是刻意取最小阈值;要「短于 N 字也算空」可自行调高。
|
|
106
|
+
|
|
107
|
+
### 验证
|
|
108
|
+
|
|
109
|
+
- `node test/guard.test.mjs`:43 个用例全通过(新增 9 个:header 判根、notice 不取消、
|
|
110
|
+
前台白名单、`fail-closed`、inbox 去重、跨重启限流、预算落盘、`foregroundKinds`、日志压平)。
|
|
111
|
+
- `node scripts/check-metadata.mjs`:通过;负向验证把上界改回 `<0.3.0` 会失败。
|
|
112
|
+
|
|
113
|
+
## [0.2.0] - 2026-11-01
|
|
114
|
+
|
|
115
|
+
**更名发布**,功能与 0.1.6 完全一致,不含行为变化。
|
|
116
|
+
|
|
117
|
+
### 变更
|
|
118
|
+
|
|
119
|
+
- npm 包名 `dsh-empty-turn-guard` → **`dsh-turn-recovery`**;GitHub 仓库同步更名。
|
|
120
|
+
- 运行时插件 id `empty-turn-guard` → **`turn-recovery`**(`export const name`、`dsh.bundle.patch` 的 `- id:`、profile 里覆盖用的 `- id:` 全部随之改变)。
|
|
121
|
+
- 补写消息来源 `plugin:empty-turn-guard` → **`plugin:turn-recovery`**。
|
|
122
|
+
- 系统提示段落名 `empty-turn-guard` → **`turn-recovery`**(`order` 仍为 960)。
|
|
123
|
+
- 日志文件名 `empty-turn-guard.log` → **`turn-recovery.log`**。
|
|
124
|
+
- 版本号从 0.1.6 跳到 0.2.0:包名与插件 id 都是破坏性变更(旧的 profile 注册与 `node_modules` 链接必须更新,否则宿主会静默跳过该 bundle),按语义化版本应当断代。
|
|
125
|
+
|
|
126
|
+
### 升级须知
|
|
127
|
+
|
|
128
|
+
从 0.1.x 升级需要同时改三处,缺一处宿主要么静默不加载、要么加载旧目录:
|
|
129
|
+
|
|
130
|
+
1. profile 的 `dependencies` 与 `dsh.profile.bundles` 里的包名;
|
|
131
|
+
2. profile 的 `cordis.patch.yml` 里 `- id: empty-turn-guard` → `- id: turn-recovery`(配置内容不变);
|
|
132
|
+
3. `node_modules` 里的包链接(junction)需要重建为新包名。
|
|
133
|
+
|
|
134
|
+
旧日志文件 `~/.dsh/logs/empty-turn-guard.log` 不会自动改名,可以保留作历史,也可以直接删除。
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
修复第四轮复核(DSH `0.2.1-rc.5`)确认的两个**独立状态机缺陷**,以及用户点停止不可观察、
|
|
138
|
+
日志/配置层面的若干小问题。`lib/index.js` 的判定口径没变,改的都是「状态什么时候被清/被置」。
|
|
139
|
+
|
|
140
|
+
### 修复
|
|
141
|
+
|
|
142
|
+
- **P2-A 认领不校验消息 id(中)**:`user/message` 只要 `source.kind = 'plugin:empty-turn-guard'`
|
|
143
|
+
就置 `awaitingGuardTurn = true`,而 `matched` 只写进日志。于是上次进程遗留、重放、或另一实例
|
|
144
|
+
写入的同来源消息会被当成「补写回合」,让下一个**真正的**空回合被 `stop chaining` 静默跳过。
|
|
145
|
+
现在只有 id 与在途补写一致才置位,不一致记 `NOT ours` 并忽略。
|
|
146
|
+
- **P2-B 缺少数值 turn 的 turn/end 不清状态(中)**:早退分支在 `wasGuard` 取值与清零之前
|
|
147
|
+
`return`,`awaitingGuardTurn` 永久留在 `true`。现在先取后清,再判断 turn 是否可判定。
|
|
148
|
+
(当前宿主唯一 emitter 恒给 number,属潜伏缺陷,用 mock 可复现。)
|
|
149
|
+
- **P2-C 用户点停止不可观察(低,缓解)**:宿主 `cancel()` 只清 inbox + abort,不发 session 事件,
|
|
150
|
+
900ms 窗口内仍会补写。现在订阅宿主的 `agent/inbox/discarded`,**只**在丢弃的正是我们那条
|
|
151
|
+
补写时才撤标记;宿主不广播该事件时退回旧行为,不会误伤。
|
|
152
|
+
- **日志轮转更鲁棒(低)**:Windows 上多进程持句柄时 `rename` 会 EPERM/EBUSY,现在回退
|
|
153
|
+
`truncate(0)` 并记一条日志(宁可丢历史,也不让日志无限长)。
|
|
154
|
+
- **agent 注册表对账(低)**:除 `agent/disposed` 外,注册表每增长 20 条向宿主 `agents.list()`
|
|
155
|
+
对账一次,删掉已消失的条目。
|
|
156
|
+
- **会话过滤留痕(低)**:`sessionScope='root'` 过滤掉的会话每个 id 记一条 `skip scope` 日志,
|
|
157
|
+
「插件没加载」与「被 scope 静默过滤」不再长得一样。
|
|
158
|
+
- **激活日志带版本号**:`plugin activated: pid=… version=…`,版本从自身 `package.json` 读,
|
|
159
|
+
不再需要靠「某字段是否存在」推断进程里跑的是哪一版。
|
|
160
|
+
- **`instructionText` 上限 4000 字符**:它进每回合 system prompt,超长会长期抬高输入 token。
|
|
161
|
+
|
|
162
|
+
### 测试
|
|
163
|
+
|
|
164
|
+
- `test/guard.test.mjs` 28 → **34 条**:新增 P2-A(id 不符不吞下一回合)、P2-B(缺 turn 也清状态)、
|
|
165
|
+
P2-C(丢弃自己那条 → 释放在途标记;丢弃别人的 → 不动)、激活日志版本号、`skip scope` 只记一次、
|
|
166
|
+
`instructionText` 截断。
|
|
167
|
+
- 修了测试夹具的一处失真:`guardClaim()` 过去不带 id,无法表达「认领」。
|
|
168
|
+
|
|
169
|
+
## [0.1.5] - 2026-10-29
|
|
170
|
+
|
|
171
|
+
修复桌面端 / 网页端复核发现的状态机与来源分类问题。
|
|
172
|
+
|
|
173
|
+
### 修复
|
|
174
|
+
|
|
175
|
+
- **投递 ≠ 认领(中)**:`awaitingGuardTurn` 过去在 `agent.send()` 成功时就置位,但 send 只是把消息放进
|
|
176
|
+
`next-turn` 队列;消息在认领前可能被用户停止/会话取消清掉,下一次真正的空回合会被当成「连环补写」
|
|
177
|
+
跳过。现在投递成功只记录 `nudgeMessageId`(在途),只有转录里出现 `source.kind =
|
|
178
|
+
'plugin:empty-turn-guard'` 的 `user/message`(被宿主认领)才置位 `awaitingGuardTurn`。在途补写 120 秒
|
|
179
|
+
未被认领视为过期;上一笔未认领前不再排第二笔。
|
|
180
|
+
- **后台来源白名单覆盖最新宿主(中)**:新增 `agent-instructions` / `ptc-mode` / `model-selection` /
|
|
181
|
+
`session-reference` / `skill-invocation` / `compact-checkpoint`。这些是宿主或插件写入的上下文/状态消息,
|
|
182
|
+
不是有人在说话;旧白名单会把它们当插话,取消待发补写。
|
|
183
|
+
- **文档口径**:README 的附件判定改为「助手消息含附件不算空」;补充 `dsh headless` 一次性进程的限制;
|
|
184
|
+
更新 2026-10-29 的实测样本。
|
|
185
|
+
|
|
186
|
+
### 测试
|
|
187
|
+
|
|
188
|
+
- `test/guard.test.mjs` 从 27 条扩到 **28 条**:新增「投递 ≠ 认领」用例;后台注入用例覆盖最新宿主的
|
|
189
|
+
6 个来源;预算/连环补写用例改为显式模拟宿主认领。
|
|
190
|
+
|
|
191
|
+
### 验证
|
|
192
|
+
|
|
193
|
+
- 用真实 DSH `0.2.1-rc.3` 宿主 + 假模型做端到端验证:turn 1 返回 ZWSP → 插件 `nudge DELIVERED` →
|
|
194
|
+
turn 2 出现 `plugin:empty-turn-guard` 的 `user/message` → 模型返回可见正文,`wasGuard=true`。
|
|
195
|
+
桌面端生产日志仍未遇到 `completed` 空回合,链路已由 e2e 覆盖。
|
|
196
|
+
|
|
197
|
+
## [0.1.4] - 2026-10-29
|
|
198
|
+
|
|
199
|
+
修复第三轮评审(技术方案评审团:架构 / 应用安全 / 现实核对)确认的缺陷。核心是**空回合判定本身**:
|
|
200
|
+
0.1.3 之前漏判「UI 空白但 `visibleChars >= 1`」的回合,而那正是本插件唯一要救的场景。
|
|
201
|
+
|
|
202
|
+
### 修复
|
|
203
|
+
|
|
204
|
+
- **零宽集改用 Unicode 属性(高)**:0.1.3 的手写码位清单漏了 `U+2066–U+2069`(双向隔离符)、`U+E0001`、
|
|
205
|
+
`U+E0020–U+E007F`(tag 字符)、`U+1D173–U+1D17A`(音乐控制符)——这些字符渲染为空白却被计成正文,
|
|
206
|
+
回合因此不补写。现在改用 `\p{Default_Ignorable_Code_Point}` 并补 `U+2800`/`U+3164`/`U+FFA0` 三个
|
|
207
|
+
不属于 DI 的填充符,实测不误伤中文/英文/数字/标点。
|
|
208
|
+
- **`inject` 假投递(高)**:`agent.inject(m)` 实际是 `send(m, 'next-step', false)`——**不唤醒**,
|
|
209
|
+
消息躺在 next-step 队列里等下一个回合认领。旧实现把它当第三级兜底,于是日志写 `DELIVERED`、
|
|
210
|
+
`awaitingGuardTurn` 置 `true`,却没有任何新回合发生,下一次真空回合还会被当成「连环补写」跳过。
|
|
211
|
+
0.1.4 起只保留会唤醒的 `send(next-turn, wakeup)` 与等价的 `followup`,全部失败就明确记 `NOT DELIVERED`。
|
|
212
|
+
- **热重载中途误判(中)**:0.1.3 去掉了 `assistantMessages > 0` 门槛后,插件在回合中途加载/热重载时
|
|
213
|
+
看不到该回合早前的助手消息,`visibleChars = 0` 会被当成空回合而发假提醒。新增 `sawTurnStart`,
|
|
214
|
+
没见过 `turn/start` 的回合一律不判定(日志 `turn/start not observed`)。
|
|
215
|
+
- **日志 tag 恒定(中)**:根会话 id 形如 `session-<uuid>`,`id.slice(0, 8)` 恒为 `session-`,多会话无法区分。
|
|
216
|
+
现在先剥 `session-` 前缀再取 8 位。
|
|
217
|
+
- **图片附件回合误判(低)**:只有 `image` / `file` / `attachment` / `audio` / `video` 的回合在界面上并非空白,
|
|
218
|
+
不再判空(`tool-call`、`reasoning` 仍按空回合处理)。
|
|
219
|
+
- **`turn` 缺失(低)**:`turn/end` 没有数值 `turn` 时直接跳过判定(旧实现拿 `s.turn` 兜底,两者都为 `null`
|
|
220
|
+
会命中 `guardSentForTurn === turn` 而静默跳过)。
|
|
221
|
+
- **会话范围(低)**:默认只处理根会话(`/^session-/`)。子代理会话(裸 UUID)「只调工具不回正文」是正常收尾,
|
|
222
|
+
社交桥会话(`social-*`)也不是空回合,都不再补写。需要旧行为可设 `sessionScope: 'all'`。
|
|
223
|
+
- **插话判定(低)**:`runtime-context` / `time-context` / `skill-catalog` / `dsh-mnemon` 与 `plugin:*`
|
|
224
|
+
视为背景注入,不取消待发补写;其余来源(真人、子代理回报、微信/社交桥)都取消。
|
|
225
|
+
- **兜底日志不轮转(低)**:`maybeRotate` 现在对主路径和 `os.tmpdir()` 兜底路径都生效,轮转前显式删除旧 `.1`
|
|
226
|
+
(避免 Windows 上覆盖失败)。
|
|
227
|
+
- **`engines.dsh` 加上界**:`>=0.2.1-rc.1 <0.3.0`。原来没有上界,将来 0.3.0 起 API 若变,插件会静默加载失败或行为异常。
|
|
228
|
+
|
|
229
|
+
### 测试
|
|
230
|
+
|
|
231
|
+
- `test/guard.test.mjs` 从 19 条扩到 **27 条**。新增:中途加载不判定、图片附件不判空、`turn` 缺失、
|
|
232
|
+
会话范围(root/all)、后台注入不取消 / 社交桥取消、只有 `inject` 可用时不算投递成功、
|
|
233
|
+
DI 码位(`U+2066`/`U+E0020`/`U+1D173`/`U+E0100`/`U+FFA0`)、日志 tag 剥前缀。
|
|
234
|
+
|
|
235
|
+
### 已知未解
|
|
236
|
+
|
|
237
|
+
- 补写消息仍以 `role:'user'` 落进持久转录(`source.kind = 'plugin:empty-turn-guard'`)。宿主
|
|
238
|
+
`dsh-session` 对 `source.kind` 无枚举校验,桌面 UI 会把非 `user` 来源渲染成 `kind:'context'` 的上下文块
|
|
239
|
+
而非用户气泡,但转录层面它仍是一条 user 消息——插件侧无法改 `role`,只能靠 `source` 标注。
|
|
240
|
+
- 生产环境至今 **0 次**真实补写投递(日志 `nudge DELIVERED` 为 0),链路正确性仍缺一次端到端演练。
|
|
241
|
+
|
|
242
|
+
## [0.1.3] - 2026-10-26
|
|
243
|
+
|
|
244
|
+
修复评审(技术方案评审团:架构 / 应用安全 / 现实核对)发现的缺陷,重点是空回合补写的状态机。
|
|
245
|
+
|
|
246
|
+
### 修复
|
|
247
|
+
|
|
248
|
+
- **补写状态机(高)**:`awaitingGuardTurn` 在三条早退路径上不清零——真人插话取消、补写回合非 `completed`(`aborted` / `error` / `max-tokens`)、三级投递全失败——导致**下一次真正的空回合被误判成「连环补写」而静默跳过**,日志还写 `stop chaining`。现在 `turn/end` 入口先取 `wasGuard` 再无条件清零;投递失败也显式清零。
|
|
249
|
+
- **`runtime-context` 被当成真人插话(中)**:宿主每一步都会 append 一条 `source.kind === 'runtime-context'` 的 `user/message`(`dsh-agent-loop`),旧实现把所有非本插件的 `user/message` 都当成真人取消补写,实际上补写几乎必被取消。现在只有 `source.kind === 'user'`(`source` 缺失时保守视为真人)才取消。
|
|
250
|
+
- **小时预算被取消操作吃掉(中)**:旧实现「调度即记账」,取消/失败也占额度;现在按**投递成功**记账。
|
|
251
|
+
- **零宽字符集过窄(中)**:补上 `U+3164`(韩文填充符,最常被当作「隐形字」)、`U+2800`(盲文空白)、`U+180B–U+180E`、`U+FE00–U+FE0F`、`U+2065`、`U+206A–U+206F`、`U+FFA0`、`U+E0100–U+E01EF`、`U+034F`、`U+061C`、`U+115F`、`U+1160`、`U+17B4`、`U+17B5`、`U+2028`、`U+2029` 等。
|
|
252
|
+
- **配置语义**:`maxNudgesPerHour: 0` 现在真的关闭补写(旧实现被 `|| 3` 吞成 3);`delayMs: "abc"` 等非法值回落默认值(旧实现 `|| 0` 变成 0 毫秒);`enabled: "false"` 字符串现在真的关闭(旧实现只认布尔 `false`)。
|
|
253
|
+
- **回合竞态**:空回合结束后 900ms 内若新回合自己起来,旧实现会把补写插进别人的回合;现在 `turn/start` 取消过期定时器,定时器触发时再校验 `pendingTurn` 是否仍等于当前回合。
|
|
254
|
+
- **漏判**:没有任何 `assistant/message` 的 `completed` 回合(`visibleChars === 0`)旧实现因为 `assistantMessages > 0` 前置条件而漏判,现在会补写。
|
|
255
|
+
- **热启用惰性**:旧实现只靠 `agent/created` 填 agent 表,插件热启用时存量会话永远没有 agent、补写全被 `SKIPPED`。现在启动时向宿主 `agents` 注册表 `list()` 回填一次。
|
|
256
|
+
- **日志**:跟随 `DSH_HOME`(旧实现硬编码 `os.homedir()`)、超过 5 MB 自动轮转、每行带会话 id 前 8 位与 `wasGuard`。
|
|
257
|
+
|
|
258
|
+
### 变更
|
|
259
|
+
|
|
260
|
+
- 补写消息正文不再要求模型「不要提及这条提醒」:该消息以 `role:'user'` 落进持久转录,隐瞒来源属于破坏转录完整性。现在明确写「插件自动补发」,并带 `source = { kind: 'plugin:empty-turn-guard', plugin, reason }`。
|
|
261
|
+
|
|
262
|
+
### 测试
|
|
263
|
+
|
|
264
|
+
- `test/guard.test.mjs` 从 9 条扩到 **19 条**(此处原写「18 条」有误),并修正**事件载荷形状**:宿主 `session.append(type, data)` 的 `data` 就是消息本体(`data.source.kind`),旧用例包了一层 `{ message: { source } }`,导致「真人插话取消」「plugin 来源不参与判定」两条用例实际空转。新增:非 completed 后不污染下一回合、投递全失败后可恢复、`runtime-context` 不取消、新回合丢弃过期补写、配置 0/非法值语义、扩展零宽集、热启用回填、卸载清理。
|
|
265
|
+
|
|
266
|
+
### 明确不做
|
|
267
|
+
|
|
268
|
+
- `inject = ['systemPrompt']` 保持硬依赖。本版本 `@deepseek-ai/cordis` 的 `Inject.resolve` 只接受数组或「name → config 映射」,**没有 `required`/`optional` 语义**;改成 `{ optional: [...] }` 会被当成「等一个叫 optional 的服务」,插件永不激活。
|
|
269
|
+
|
|
270
|
+
## [0.1.2] - 2026-10-25
|
|
271
|
+
|
|
272
|
+
放宽宿主版本门槛:功能无改动。
|
|
273
|
+
|
|
274
|
+
### 变更
|
|
275
|
+
|
|
276
|
+
- `engines.dsh` 从 `>=0.2.1-rc.2` 放宽到 `>=0.2.1-rc.1`。
|
|
277
|
+
依据:本插件用到的宿主 API(`ctx.systemPrompt.section`、`agent.send` / `followup` / `inject`、
|
|
278
|
+
`agent/created`、`session/event`、`dsh.bundle.patch` 加载)在 DSH `0.2.1-rc.1` / `rc.2` / `rc.3`
|
|
279
|
+
三个版本的实现**逐字节相同**(`dsh-system-prompt`、`dsh-agent-loop`、`dsh-agent`、`dsh-session`、
|
|
280
|
+
`dsh-app-boot`、`dsh-plugin-manager`、`dsh-web-app` 的 `lib/index.js` md5 三版本一致,差异仅在
|
|
281
|
+
`package.json` 的版本号字段)。所以 rc.1 上代码本来就能跑,旧门槛只是把它挡在门外——
|
|
282
|
+
宿主 `dsh-app-boot` 的版本闸门不满足时会把 bundle 收进 `skippedBundles`,启动只打一行
|
|
283
|
+
`dsh: skipping profile bundle "dsh-empty-turn-guard": …` 然后**静默不加载**。
|
|
284
|
+
|
|
285
|
+
### 注意
|
|
286
|
+
|
|
287
|
+
- **不要**把范围写成 `>=0.2.1`。semver 规则里预发布版本不满足「不含预发布标识的同段范围」,
|
|
288
|
+
`0.2.1-rc.3` 对 `>=0.2.1` 判为 `false`,那样会把 rc.1 / rc.2 / rc.3 全部挡掉。
|
|
289
|
+
|
|
290
|
+
## [0.1.1] - 2026-10-25
|
|
291
|
+
|
|
292
|
+
发布规范化版本:功能与 0.1.0 一致,补齐工程与元数据。
|
|
293
|
+
|
|
294
|
+
### 新增
|
|
295
|
+
|
|
296
|
+
- `test/guard.test.mjs`:9 条契约测试,用 mock ctx 驱动真实 `apply()`,不依赖宿主即可回归。
|
|
297
|
+
覆盖零宽空回合补写、正常回合不干预、非 `completed` 不补写、补写回合再空不连环、
|
|
298
|
+
真人插话取消、`send` 失败降级 `followup`、`plugin:` 来源消息不参与判定。
|
|
299
|
+
- `scripts/check-metadata.mjs`:发布前元数据检查,校验字段齐全、`files` 路径真实存在、
|
|
300
|
+
无 `TODO` 占位符、`cordis.patch.yml` 的 `- insert:` 与包名对齐、入口可 `import`。
|
|
301
|
+
- `CHANGELOG.md`(本文件)。
|
|
302
|
+
|
|
303
|
+
### 修正
|
|
304
|
+
|
|
305
|
+
- 重新统计并修正 README 里的实测样本数据(原 0.1.0 写的是 738 回合 / 102 空 / 13.8%,
|
|
306
|
+
该口径把只有工具调用的步骤也算成了空回合)。准确口径:208 会话 / 766 回合,
|
|
307
|
+
可见正文为空的回合 39 个(5.1%),其中 `completed` 的 8 个(1.2%)才是本插件处理的对象。
|
|
308
|
+
|
|
309
|
+
### 变更
|
|
310
|
+
|
|
311
|
+
- `package.json` 补齐发布元数据:`repository` / `homepage` / `bugs` / `author` / `exports` /
|
|
312
|
+
`engines` / `publishConfig` / `scripts`。
|
|
313
|
+
- `prepublishOnly` 串联 `check-metadata` 与 `test`,检查不过不允许发布。
|
|
314
|
+
|
|
315
|
+
## [0.1.0] - 2026-10-25
|
|
316
|
+
|
|
317
|
+
首个可用版本。
|
|
318
|
+
|
|
319
|
+
### 新增
|
|
320
|
+
|
|
321
|
+
- **C 空回合保护**:监听 `session/event`,在 `turn/end`(`reason.kind === 'completed'`)时
|
|
322
|
+
累计本回合所有 `assistant/message` 的可见正文;去掉零宽字符后为空则自动排一条
|
|
323
|
+
`target: 'next-turn'` 的补写消息(`agent.send(msg, 'next-turn', true)`,
|
|
324
|
+
失败依次降级 `followup` → `inject`),让模型在下一回合补出正文。
|
|
325
|
+
- **A 收尾指令注入**:通过 `systemPrompt.section({ name, order: 960, interpolate: false })`
|
|
326
|
+
注入 222 字指令,要求每回合以看得见的正文收尾、禁止用零宽字符或空白充当回答。
|
|
327
|
+
- 防误伤:同回合最多补写一次、补写回合再空即停、每小时最多 3 次、
|
|
328
|
+
定时器触发前收到真人消息立即取消、`plugin:` 来源消息不参与判定。
|
|
329
|
+
- 日志落 `~/.dsh/logs/empty-turn-guard.log`,主路径不可写时兜底到系统临时目录。
|
|
330
|
+
|
|
331
|
+
### 说明
|
|
332
|
+
|
|
333
|
+
- 修复目标问题:模型把输出预算烧在 reasoning 上、正文只返回一个 `U+200B`,
|
|
334
|
+
而 DSH 的 `EMPTY_RESPONSE` 判空因「文本非空」不触发,回合被记为 `completed` 但界面空白。
|