dsh-live-compact 1.0.7 → 1.0.9
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/README.md +87 -9
- package/cooldown.js +106 -0
- package/gate.js +3 -3
- package/index.js +326 -118
- package/package.json +6 -1
- package/plan.js +385 -82
- package/raw.js +91 -18
- package/shadow.js +192 -2
package/README.md
CHANGED
|
@@ -92,6 +92,57 @@ dsh-live-compact --version
|
|
|
92
92
|
|
|
93
93
|

|
|
94
94
|
|
|
95
|
+
## 1.0.9
|
|
96
|
+
|
|
97
|
+
官方大压缩的三项预设参数可以在启动前用环境变量覆盖:`SPINE_THRESHOLD_RATIO`、`SPINE_HEADROOM_TOKENS`、`SPINE_RETAIN_TOKENS`。云端默认不动,仍是 `0.8`、`65536`、`8192`,不按地址自动改小。TUI 的 `/settings` 目前没有这一项。
|
|
98
|
+
|
|
99
|
+
请求头通常只有 provider 名。云端和本地按这一家的地址区分:当前配置里的 `baseURL` 优先,没有再看官方 DeepSeek 和内置目录。公网地址按云端切,回环和内网按本地。读不到地址仍按本地,不猜。这不改官方大门的 headroom。`SPINE_ENGINE=local` 或 `cloud` 仍可强制指定。
|
|
100
|
+
|
|
101
|
+
发版门禁是 `npm run test:gate`,不连模型。8 组 31 轮在 `test/live-fill`,要连模型。这两部分都在 GitHub 测试集里。Pi 移植不在这道门禁里。
|
|
102
|
+
|
|
103
|
+
## 1.0.9 测评
|
|
104
|
+
|
|
105
|
+
云端看题对不对,以及每轮第一个 `step_end` 的缓存命中 `cacheRead / (cacheRead + input)`。本地不看缓存命中,看题对不对和切掉的 token。切掉是 `shadowedTokenCount`。费用是离峰用量估算,不是账单。
|
|
106
|
+
|
|
107
|
+
DS V4.1 Flash,8 组 31 轮。184/184,跳过 0。缓存命中 98.22%。题末最高 35334,平均 28984。切掉 97306。估算费用 $0.208。
|
|
108
|
+
|
|
109
|
+
GLM 5.3 Flash,同一套 8 组。184/184,跳过 0。同一公式缓存命中 74.66%。第 6、8 组几乎每轮只 `read` 一次全文,刀落在三轮以前的那份全文上,前缀被改写。切掉 481483。这是模型用法,规则没有为它改。
|
|
110
|
+
|
|
111
|
+
Coder 390 27B,只跑了 3 组 31 轮,没有八组并行。填日志 69/69。切掉 20635,都在第 30 轮:组 1 是 10503,组 2 是 5106,组 3 是 5026。墙钟 29 分、20 分、86 分。
|
|
112
|
+
|
|
113
|
+

|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+
`cordis.patch.yml` 是静态整块替换,不读这三个环境变量。要在这里改 `headroomTokens`,需直接改配置块,例如:
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
- id: compaction-spine
|
|
125
|
+
name: dsh-live-compact
|
|
126
|
+
config:
|
|
127
|
+
auto: true
|
|
128
|
+
thresholdRatio: 0.8
|
|
129
|
+
headroomTokens: 16384
|
|
130
|
+
retainTokens: 8192
|
|
131
|
+
compactionRetries: 0
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 1.0.8
|
|
135
|
+
|
|
136
|
+
同一套 8 组、184 个填日志回合。真实 DeepSeek。跳过工具 0,WARN 184/184,NOTICE 184/184。缓存命中 98.23%,1.0.6 是 97.88%,原版是 98.51%。题末平均 2.6 万,最高 3.6 万,最低 1.6 万。1.0.6 最高 3.5 万。原版最高 12.6 万。费用按离峰用量估算,1.0.8 是 $0.209,1.0.6 是 $0.211,原版是 $0.229,不是账单。切掉的原文 11.4 万 token。原版没有压缩,8 组题末加起来积累了 34.9 万。
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
卡片不再使用官方的 `<compacted-summary>` 和 `compact-checkpoint`。官方压缩会把那种节点当成上一版摘要再摘一次,切片摘要会越压越短。新卡片用 `spine-checkpoint`。已经写过的旧卡片仍会停住,不会再切一遍。
|
|
141
|
+
|
|
142
|
+
遇到已有卡片就停止往前扩。这是故意的。卡片已经换过一次,再穿过去会改掉已经进缓存的前缀。最新一段不够 1000,就跳过,不回头凑。
|
|
143
|
+
|
|
144
|
+
单个工具结果超过 3.2 万 token 才只留指针。默认不改。要自己调,启动前设 `SPINE_POINTER_TOKENS`,例如 `8000`。不按占比算。
|
|
145
|
+
|
|
95
146
|
## 1.0.7 长填日志
|
|
96
147
|
|
|
97
148
|
1.0.7 跑了同一套 8 组。真实 DeepSeek。184 个填日志回合都调用了工具。WARN 183/184,NOTICE 184/184,假 NOTICE 是 0。8 组都切过,一共 43 刀。卡片 43 张都有会话序号,没有备份路径。
|
|
@@ -100,7 +151,7 @@ dsh-live-compact --version
|
|
|
100
151
|
|
|
101
152
|

|
|
102
153
|
|
|
103
|
-
和已发布的 1.0.6
|
|
154
|
+
和已发布的 1.0.6、以及上次同一套题的官方压缩比。跳过工具都是 0。费用按离峰用量估算,1.0.7 是 $0.249,1.0.6 是 $0.211,原版是 $0.229,不是账单。缓存命中 1.0.7 是 96.10%,1.0.6 是 97.88%,原版是 98.51%。题末上下文 1.0.7 平均 2.3 万、最高 3.1 万、最低 1.9 万,8 组都在 3.2 万以内。1.0.6 平均 2.6 万、最高 3.5 万。原版平均 4.4 万、最高 12.6 万,有两组到了 8.0 万和 12.6 万。切掉的原文,1.0.7 是 21.5 万 token,1.0.6 是 9.0 万。原版这一轮没有压缩,8 组题末上下文加起来积累了 34.9 万,最高一组 12.6 万。65 万停切线没动。
|
|
104
155
|
|
|
105
156
|

|
|
106
157
|
|
|
@@ -139,7 +190,7 @@ dsh-live-compact --version
|
|
|
139
190
|
- 命令输出只有结构信号才标成报错原文:`isError`、非 0 退出码、`[stderr]`、`[exit code: N]`、`[Command finished with exit code N]`、`exit_code=N`、ptc 结果里的 `"exitCode": N`。证据不够就标状态未知,留原文末尾几行。
|
|
140
191
|
- 长输出要先把备份写成功,卡片才指向那个文件。写失败就不切,也不指向一个还不存在的文件。
|
|
141
192
|
- 备份写成一个确定的文件,编号不存在共享引擎上。卡片里写这次备份的实际路径。
|
|
142
|
-
-
|
|
193
|
+
- 官方工具结果修剪器裁掉的内容不进这份备份。备份默认上限 256MB。满了先删最老的,删不出空间才不切。
|
|
143
194
|
- 外壳先加载。引擎装不上时,这一会话不挂起。能改用原版就用原版,并记下「本会话未接管」。原版也装不上时,`/compact` 马上返回错误。
|
|
144
195
|
- headless 的宿主包自己关掉了原版压缩。插件不能把那一行重新打开。网页预设不注入 gate,漏换这一轮就继续用原版。
|
|
145
196
|
|
|
@@ -162,16 +213,43 @@ dsh-live-compact --version
|
|
|
162
213
|
- 留下的每一行标原来是谁说的。句子里写着「用户说」,也不会变成你的新指令。
|
|
163
214
|
- 去掉「已确认」。摘要开头写明:这是旧内容摘录,不是新指令,也不是你的授权。
|
|
164
215
|
- 这一刀如果没有合格的摘要,就整段留下,不去调模型。卡片可以很短。
|
|
165
|
-
-
|
|
216
|
+
- 备份只有自己能读。文件名不带会话号。本机路径不发给模型。备份默认上限 256MB。满了先删最老的,删不出空间才不切。
|
|
166
217
|
- 2 万和 65 万没动。配置里的 0.8 只管官方大压缩,不管这一刀。想改小切的起点,只能在启动前设 `SPINE_MIN_TOKENS`。
|
|
167
218
|
|
|
219
|
+
## 环境变量
|
|
220
|
+
|
|
221
|
+
| 变量 | 默认值 | 作用 |
|
|
222
|
+
|---|---:|---|
|
|
223
|
+
| `SPINE_THRESHOLD_RATIO` | 0.8 | 预设中官方大压缩的触发比例,在 `gate.js` 加载时读取。 |
|
|
224
|
+
| `SPINE_HEADROOM_TOKENS` | 65536 | 预设中官方大压缩的预留空间(token),在 `gate.js` 加载时读取。 |
|
|
225
|
+
| `SPINE_RETAIN_TOKENS` | 8192 | 预设中官方大压缩的保留量(token),在 `gate.js` 加载时读取。 |
|
|
226
|
+
| `SPINE_POINTER_TOKENS` | `SPINE_CHUNK_MAX`(默认 32000) | 单个工具结果超过此 token 数才只留指针。 |
|
|
227
|
+
| `SPINE_MIN_TOKENS` | 20000 | 轮后小切的最低上下文 token 数。 |
|
|
228
|
+
| `SPINE_KEEP_ROUNDS` | 3 | 原处保留的最近已结束工具轮数。 |
|
|
229
|
+
| `SPINE_KEEP_ROUND_TOKENS` | 32000 | 云端收尾的停止线;最近 3 个已结束工具轮始终留在原文。 |
|
|
230
|
+
| `SPINE_SHADOW_DIR` | `~/.dsh/compaction-spine/shadow` | 备份目录;设了 `DSH_HOME` 时以它替代 `~/.dsh`。 |
|
|
231
|
+
| `SPINE_SHADOW_MAX_BYTES` | 268435456 | 正数直接作为备份字节上限,优先于 `SPINE_SHADOW_MAX_MB`。 |
|
|
232
|
+
| `SPINE_SHADOW_MAX_MB` | 256 | 未设置正数字节上限时,按 MB 设置备份上限。 |
|
|
233
|
+
| `SPINE_ENGINE` | 自动识别 | 设为 `cloud` 或 `local` 可强制切法,其他值忽略。 |
|
|
234
|
+
| `SPINE_THUNDER` | 关闭 | 设为 `1` 时启用雷霆摘要路径。 |
|
|
235
|
+
| `SPINE_THUNDER_MODEL` | `flash` | 设为 `27b` 时使用对应模型类型,其他值仍为 `flash`。 |
|
|
236
|
+
| `SPINE_ENGINE_FAIL` | 无 | **仅供测试**:用 `import`、`construct`、`after-super` 注入引擎故障。 |
|
|
237
|
+
| `SPINE_STOCK_FAIL` | 无 | **仅供测试**:设为 `1` 时禁止回退原版引擎。 |
|
|
238
|
+
| `SPINE_CHUNK_MIN` | 1000 | 一刀至少包含的 token 数。 |
|
|
239
|
+
| `SPINE_CHUNK_MAX` | 32000(`RAIL_TOKENS`) | 一刀最多包含的 token 数,也是指针阈值的默认值。 |
|
|
240
|
+
| `SPINE_COOLDOWN_MS` | 0 | 官方压缩后的额外时间窗口;仅正数开启,未设置、空串、非数字、0 或负数均关闭。 |
|
|
241
|
+
|
|
242
|
+
每个新的 `compact-checkpoint` 只让轮后小切跳过一次,`spine-checkpoint` 不触发这个跳过。
|
|
243
|
+
|
|
244
|
+
`gate.js` 的官方压缩预设默认使用 `thresholdRatio: 0.8`、`headroomTokens: 65536`、`retainTokens: 8192`,可在启动前用对应环境变量覆盖。这些数值只管官方大压缩,不改小切切点。
|
|
245
|
+
|
|
168
246
|
## 安装
|
|
169
247
|
|
|
170
248
|
从 npm 装。headless 和网页是两个 profile,要各装一次。桌面端是另一个 profile,普通 `dsh plugin --profile desktop` 会被拒绝。
|
|
171
249
|
|
|
172
250
|
```sh
|
|
173
|
-
dsh plugin --profile headless add dsh-live-compact@1.0.
|
|
174
|
-
dsh plugin --profile web add dsh-live-compact@1.0.
|
|
251
|
+
dsh plugin --profile headless add dsh-live-compact@1.0.9
|
|
252
|
+
dsh plugin --profile web add dsh-live-compact@1.0.9
|
|
175
253
|
```
|
|
176
254
|
|
|
177
255
|
想先装到本机看版本:
|
|
@@ -181,12 +259,12 @@ npm install -g dsh-live-compact
|
|
|
181
259
|
dsh-live-compact --version
|
|
182
260
|
```
|
|
183
261
|
|
|
184
|
-
备用:上不了 npm 的,用国内镜像站的包。1.0.2 到 1.0.
|
|
262
|
+
备用:上不了 npm 的,用国内镜像站的包。1.0.2 到 1.0.8 的旧链接仍在。
|
|
185
263
|
|
|
186
264
|
```sh
|
|
187
|
-
dsh plugin --profile headless add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.
|
|
188
|
-
dsh plugin --profile web add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.
|
|
189
|
-
npm install -g --force "https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.
|
|
265
|
+
dsh plugin --profile headless add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.9.tgz
|
|
266
|
+
dsh plugin --profile web add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.9.tgz
|
|
267
|
+
npm install -g --force "https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.9.tgz"
|
|
190
268
|
dsh-live-compact --version
|
|
191
269
|
```
|
|
192
270
|
|
package/cooldown.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// cooldown.js — 「某类系统事件刚发生过,先让一让」的通用判定
|
|
2
|
+
//
|
|
3
|
+
// 【为什么单独成文件】
|
|
4
|
+
// 这个判断与压缩引擎、卡片格式、切块选择都无关,只有两个输入:会话的只读节点表 + 一个
|
|
5
|
+
// `source.kind` 字符串。放成独立文件后:
|
|
6
|
+
// · 调用方(index.js)只加一行判定,不碰任何既有逻辑
|
|
7
|
+
// · 整份文件可以原样复制到别的项目(不认识压缩、不 import 宿主包、无全局状态)
|
|
8
|
+
//
|
|
9
|
+
// 【两个判据,主判据是「这条官方卡片处理过没有」】
|
|
10
|
+
// 只看时间窗口会漏掉一种真实情形:官方压缩后长时间没消息(比如一小时),再来消息时
|
|
11
|
+
// 窗口早过了,spine 立刻切——而那正是要挡的一刀(官方摘要还在 surface 里,容易被卷走)。
|
|
12
|
+
// 所以主判据用 seq:每出现一条**新的**官方卡片,调用方就跳过一次动作并记住该 seq;
|
|
13
|
+
// 同一条卡片只跳一次,不会永久压制。
|
|
14
|
+
// 时间窗口作为可选的额外兜底(`SPINE_COOLDOWN_MS`,默认 0 = 只用 seq 语义),
|
|
15
|
+
// 用于「压缩后短时间内反复 idle」的场景。
|
|
16
|
+
// 两者都不引入轮次概念——轮次要定义「什么算一轮」「中断算不算」,成本高且难测。
|
|
17
|
+
|
|
18
|
+
/** 默认冷却窗口:5 分钟。具名常量,不散落魔数。 */
|
|
19
|
+
export const DEFAULT_COOLDOWN_MS = 300000;
|
|
20
|
+
|
|
21
|
+
/** 官方压缩卡片使用的 `source.kind`(宿主契约字段,不是文本标记)。 */
|
|
22
|
+
export const OFFICIAL_COMPACTION_KIND = "compact-checkpoint";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 解析冷却窗口配置。
|
|
26
|
+
* @param {Record<string, string|undefined>} [env] 环境变量表(测试可注入)
|
|
27
|
+
* @returns {number} 毫秒;**0 表示关闭时间窗口**(负数同样视为关闭)
|
|
28
|
+
*/
|
|
29
|
+
export function resolveCooldownMs(env = process.env) {
|
|
30
|
+
const raw = Number(env?.SPINE_COOLDOWN_MS);
|
|
31
|
+
if (!Number.isFinite(raw)) return 0;
|
|
32
|
+
return raw <= 0 ? 0 : Math.floor(raw);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 会话里「最近一次指定 source.kind 的节点」距今多少毫秒。
|
|
37
|
+
*
|
|
38
|
+
* 从 surface 尾部往前找第一条匹配的 `user/message`:只读 `session.surface.nodes` 与
|
|
39
|
+
* `session.eventAt(seq)` 这两个宿主稳定接口,因此对任何"会在会话里留下卡片"的机制都适用。
|
|
40
|
+
*
|
|
41
|
+
* @param {object} session 宿主会话(需有 surface.nodes 与 eventAt)
|
|
42
|
+
* @param {string} kind 目标 `source.kind`
|
|
43
|
+
* @param {number} [now] 当前时间(毫秒),便于测试注入
|
|
44
|
+
* @returns {number} 毫秒;找不到返回 `Infinity`(视为"从未发生")
|
|
45
|
+
*/
|
|
46
|
+
export function lastSourceKindAgeMs(session, kind, now = Date.now()) {
|
|
47
|
+
const nodes = session?.surface?.nodes;
|
|
48
|
+
if (!Array.isArray(nodes) || nodes.length === 0) return Infinity;
|
|
49
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
50
|
+
const event = session.eventAt?.(nodes[index]);
|
|
51
|
+
if (event?.type !== "user/message") continue;
|
|
52
|
+
if (event?.data?.source?.kind !== kind) continue;
|
|
53
|
+
const time = event.time;
|
|
54
|
+
if (typeof time !== "number") return Infinity;
|
|
55
|
+
return Math.max(0, now - time);
|
|
56
|
+
}
|
|
57
|
+
return Infinity;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 会话里「最近一次指定 source.kind 的节点」的 seq。
|
|
62
|
+
*
|
|
63
|
+
* 比时间更可靠的判据:只要出现**新的**官方卡片,seq 就会变;与消息间隔多久无关。
|
|
64
|
+
*
|
|
65
|
+
* @param {object} session 宿主会话(需有 surface.nodes 与 eventAt)
|
|
66
|
+
* @param {string} kind 目标 `source.kind`
|
|
67
|
+
* @returns {number|undefined} seq;找不到返回 undefined
|
|
68
|
+
*/
|
|
69
|
+
export function lastSourceKindSeq(session, kind) {
|
|
70
|
+
const nodes = session?.surface?.nodes;
|
|
71
|
+
if (!Array.isArray(nodes) || nodes.length === 0) return undefined;
|
|
72
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
73
|
+
const seq = nodes[index];
|
|
74
|
+
const event = session.eventAt?.(seq);
|
|
75
|
+
if (event?.type !== "user/message") continue;
|
|
76
|
+
if (event?.data?.source?.kind !== kind) continue;
|
|
77
|
+
return seq;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 官方压缩卡片的年龄(毫秒)。
|
|
84
|
+
* @param {object} session 宿主会话
|
|
85
|
+
* @param {number} [now] 当前时间,便于测试注入
|
|
86
|
+
* @returns {number} 毫秒;没有官方卡片返回 `Infinity`
|
|
87
|
+
*/
|
|
88
|
+
export function officialCompactionAgeMs(session, now = Date.now()) {
|
|
89
|
+
return lastSourceKindAgeMs(session, OFFICIAL_COMPACTION_KIND, now);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 是否处在冷却期——是则调用方本轮应当跳过动作。
|
|
94
|
+
*
|
|
95
|
+
* @param {object} session 宿主会话
|
|
96
|
+
* @param {{windowMs?: number, now?: number, kind?: string}} [options]
|
|
97
|
+
* windowMs ≤ 0 表示关闭(恒返回 false);kind 缺省为官方压缩卡片
|
|
98
|
+
* @returns {boolean} true = 刚发生过,本轮让一让
|
|
99
|
+
*/
|
|
100
|
+
export function inCooldown(session, options = {}) {
|
|
101
|
+
const windowMs = options.windowMs ?? DEFAULT_COOLDOWN_MS;
|
|
102
|
+
if (!(windowMs > 0)) return false;
|
|
103
|
+
const kind = options.kind ?? OFFICIAL_COMPACTION_KIND;
|
|
104
|
+
const now = options.now ?? Date.now();
|
|
105
|
+
return lastSourceKindAgeMs(session, kind, now) < windowMs;
|
|
106
|
+
}
|
package/gate.js
CHANGED
|
@@ -6,9 +6,9 @@ const PRESET_IDS = new Set(["standard", "ptc", "minimal", "cordis", "preset-stan
|
|
|
6
6
|
export const GATE_SERVICE = "liveCompactPresetGate";
|
|
7
7
|
export const PRESET_ENGINE_CONFIG = {
|
|
8
8
|
auto: true,
|
|
9
|
-
thresholdRatio: 0.8,
|
|
10
|
-
headroomTokens: 65536,
|
|
11
|
-
retainTokens: 8192,
|
|
9
|
+
thresholdRatio: Number(process.env.SPINE_THRESHOLD_RATIO || 0.8),
|
|
10
|
+
headroomTokens: Number(process.env.SPINE_HEADROOM_TOKENS || 65536),
|
|
11
|
+
retainTokens: Number(process.env.SPINE_RETAIN_TOKENS || 8192),
|
|
12
12
|
compactionRetries: 0,
|
|
13
13
|
};
|
|
14
14
|
|