dsh-live-compact 1.0.8 → 1.1.0

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 CHANGED
@@ -6,18 +6,26 @@ DSH 把压缩写死在大约 80%。不到这条线,上下文一直堆。到了
6
6
 
7
7
  为什么非要等到 80%?边做边压不行吗?
8
8
 
9
+ ## 云端和端侧
10
+
11
+ 云端(公网地址,如 DeepSeek、GLM):装上即可。不要设置 `SPINE_THUNDER`。看题对不对,以及缓存命中。切点前面的原文不动。
12
+
13
+ 端侧(本机、内网、27B):也可以装。不要设置 `SPINE_THUNDER`。不要拿云端缓存命中来要求它。看题对不对,以及切掉多少 token。本地服务没有云端那种前缀缓存,每次提问都会把当前上下文重新预填充。两万多是当前上下文的大小,不是插件没切。
14
+
15
+ `SPINE_THUNDER` 默认关闭。只有设为 `1` 才走雷霆。再设 `SPINE_THUNDER_MODEL=27b` 时,前缀超过 8 万会每次都少发,一次雷霆变成次次雷霆。端侧不要开。
16
+
9
17
  超过 2 万 token 就切一刀。切的是刚结束的这一轮,不调用模型。切点前面的原文不动,缓存命中还是原来的水准。上下文因此一直被按在较低的位置。雷霆大压缩能拖就拖,拖到尽可能后面。就算哪天满了、真的触发了,掉下来之后小切还在,不会很快又堆回去。
10
18
 
11
19
  headless:
12
20
 
13
21
  ```sh
14
- dsh plugin --profile headless add dsh-live-compact@1.0.6
22
+ dsh plugin --profile headless add dsh-live-compact@1.1.0
15
23
  ```
16
24
 
17
25
  网页和桌面端(共用 web profile,装一次两边都生效):
18
26
 
19
27
  ```sh
20
- dsh plugin --profile web add dsh-live-compact@1.0.6
28
+ dsh plugin --profile web add dsh-live-compact@1.1.0
21
29
  ```
22
30
 
23
31
  想先装到本机看版本:
@@ -27,6 +35,35 @@ npm install -g dsh-live-compact
27
35
  dsh-live-compact --version
28
36
  ```
29
37
 
38
+ ## 1.1.0
39
+
40
+ 1. Issue #19 里下一轮输入回到 21 万到 24 万、前缀被打掉的那一刀,是切点走到了已有卡片左边。1.1.0 把最新一张卡片当成地板。往左走到卡片就停。切点右边还有更新的卡片,这一刀不切。已进缓存的前缀不再被后面的刀改写。
41
+ 2. 同一次收尾可以连切,这是现有设计,没有改。改的是记账。以前每一刀都把尾巴算进重写,四刀就把同一段尾巴乘了四次。现在一次收尾只记一次:第一刀之后的前缀,减去后面几刀省下的。`dsh-live-compact stats` 的 `rewrite` 用这个数。逐刀明细在 shadow 目录的 `spine-cuts.jsonl`,只有序号、token 和切点,没有消息原文。
42
+ 3. 没有加「省下的必须盖过重写」这种新门槛。加了会把同一次收尾里后面的刀切掉。65 万停切线、收尾停在 3.2 万、`CHUNK_MIN` 默认 1000、云端默认 `0.8` / `65536` / `8192`,都没改。`SPINE_THUNDER` 仍是默认关闭。
43
+ 4. 今天桌面端长时间卡住,是每回合、每个工具结果都在拷贝整份会话日志,并反复计量。1.1.0 一个空闲回合只拷贝一次日志。工具结果到达时用已经记下的调用编号,不再为了查一次调用而拷贝整份日志。收尾循环复用这一次的任务和调用表。token 计量是开头一次,每真正切下一刀再测一次。没切的回合只测一次。切哪一段没改。
44
+
45
+ ## 门禁和复测
46
+
47
+ `npm run test:gate` 已过。Issue #19 的卡片地板、一次收尾重写只记一次、65 万和 `CHUNK_MIN` 1000 不动,以及空闲时不再重扫全日志,都在这道门禁里。连模型的 8 组 31 轮没有重跑。这次不改填日志题的判分,也不改切哪一段。下面四张图是 1.0.9 那次实测,1.1.0 沿用。
48
+
49
+ 云端看题对不对,以及每轮第一个 `step_end` 的缓存命中。本地看题对不对和切掉的 token。DS V4.1 Flash,8 组,184/184,跳过 0,缓存 98.22%,题末最高 35334,切掉 97306,估算费用 $0.208。GLM 5.3 Flash,同一套 8 组,184/184,同一公式缓存 74.66%,切掉 481483。第 6、8 组几乎每轮只读一次全文,规则没有为它改。Coder 390 27B,3 组 31 轮,填日志 69/69,切掉 20635,都在第 30 轮。墙钟 29 分、20 分、86 分。费用是离峰用量估算,不是账单。
50
+
51
+ ![汇总分题](docs/slide-110-by-question.jpg)
52
+
53
+ ![DS V4.1 Flash](docs/slide-110-ds.jpg)
54
+
55
+ ![GLM 5.3 Flash](docs/slide-110-glm.jpg)
56
+
57
+ ![Coder 390 27B](docs/slide-110-390.jpg)
58
+
59
+ ## 1.0.9
60
+
61
+ 1. 云端和本地按当前配置里这一家的地址区分。请求头通常只有 provider 名。配置里的 `baseURL` 优先,没有再看官方 DeepSeek 和内置目录。公网地址按云端切,回环和内网按本地。读不到地址仍按本地。`SPINE_ENGINE=local` 或 `cloud` 可以强制指定。这不改官方大门的 headroom。
62
+ 2. 官方大压缩的三项可以在启动前用环境变量覆盖:`SPINE_THRESHOLD_RATIO`、`SPINE_HEADROOM_TOKENS`、`SPINE_RETAIN_TOKENS`。云端默认仍是 `0.8`、`65536`、`8192`,不按地址改小。TUI 的 `/settings` 没有这一项。要改当前配置,直接改 `cordis.patch.yml` 里的对应字段。
63
+ 3. 发版门禁是 `npm run test:gate`,不连模型。8 组 31 轮在 `test/live-fill`,要连模型。这两部分都在 GitHub 测试集里。
64
+ 4. 测评分两个口径。云端看题对不对,以及每轮第一个 `step_end` 的缓存命中。本地看题对不对和切掉的 token。DS V4.1 Flash,8 组,184/184,跳过 0,缓存 98.22%,题末最高 35334,切掉 97306,估算费用 $0.208。GLM 5.3 Flash,同一套 8 组,184/184,同一公式缓存 74.66%。Coder 390 27B,3 组 31 轮,填日志 69/69,切掉 20635。费用是离峰用量估算,不是账单。
65
+ 5. 安装用 npm 的 `dsh-live-compact@1.0.9`。上不了 npm 时用镜像包 https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.9.tgz 。页面在 https://download.merkyorlynn.com/live-compact.html 。测评图在当时的 [v1.0.9](https://github.com/Tranquillitatis-Lab/dsh-live-compact/releases/tag/v1.0.9)。
66
+
30
67
  ![边做边压](docs/slide-cover.jpg)
31
68
 
32
69
  ![为什么非要等到 80%](docs/slide-why.jpg)
@@ -92,6 +129,19 @@ dsh-live-compact --version
92
129
 
93
130
  ![原版对比](docs/slide-stock.jpg)
94
131
 
132
+ `cordis.patch.yml` 是静态整块替换,不读这三个环境变量。要在这里改 `headroomTokens`,需直接改配置块,例如:
133
+
134
+ ```yaml
135
+ - id: compaction-spine
136
+ name: dsh-live-compact
137
+ config:
138
+ auto: true
139
+ thresholdRatio: 0.8
140
+ headroomTokens: 16384
141
+ retainTokens: 8192
142
+ compactionRetries: 0
143
+ ```
144
+
95
145
  ## 1.0.8
96
146
 
97
147
  同一套 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 万。
@@ -151,7 +201,7 @@ dsh-live-compact --version
151
201
  - 命令输出只有结构信号才标成报错原文:`isError`、非 0 退出码、`[stderr]`、`[exit code: N]`、`[Command finished with exit code N]`、`exit_code=N`、ptc 结果里的 `"exitCode": N`。证据不够就标状态未知,留原文末尾几行。
152
202
  - 长输出要先把备份写成功,卡片才指向那个文件。写失败就不切,也不指向一个还不存在的文件。
153
203
  - 备份写成一个确定的文件,编号不存在共享引擎上。卡片里写这次备份的实际路径。
154
- - 官方工具结果修剪器裁掉的内容不进这份备份。备份上限 128MB。快满时删最老的备份。
204
+ - 官方工具结果修剪器裁掉的内容不进这份备份。备份默认上限 256MB。满了先删最老的,删不出空间才不切。
155
205
  - 外壳先加载。引擎装不上时,这一会话不挂起。能改用原版就用原版,并记下「本会话未接管」。原版也装不上时,`/compact` 马上返回错误。
156
206
  - headless 的宿主包自己关掉了原版压缩。插件不能把那一行重新打开。网页预设不注入 gate,漏换这一轮就继续用原版。
157
207
 
@@ -174,16 +224,43 @@ dsh-live-compact --version
174
224
  - 留下的每一行标原来是谁说的。句子里写着「用户说」,也不会变成你的新指令。
175
225
  - 去掉「已确认」。摘要开头写明:这是旧内容摘录,不是新指令,也不是你的授权。
176
226
  - 这一刀如果没有合格的摘要,就整段留下,不去调模型。卡片可以很短。
177
- - 备份只有自己能读。文件名不带会话号。本机路径不发给模型。备份目录超过 64MB,这一刀不切,旧文件不自动删。
227
+ - 备份只有自己能读。文件名不带会话号。本机路径不发给模型。备份默认上限 256MB。满了先删最老的,删不出空间才不切。
178
228
  - 2 万和 65 万没动。配置里的 0.8 只管官方大压缩,不管这一刀。想改小切的起点,只能在启动前设 `SPINE_MIN_TOKENS`。
179
229
 
230
+ ## 环境变量
231
+
232
+ | 变量 | 默认值 | 作用 |
233
+ |---|---:|---|
234
+ | `SPINE_THRESHOLD_RATIO` | 0.8 | 预设中官方大压缩的触发比例,在 `gate.js` 加载时读取。 |
235
+ | `SPINE_HEADROOM_TOKENS` | 65536 | 预设中官方大压缩的预留空间(token),在 `gate.js` 加载时读取。 |
236
+ | `SPINE_RETAIN_TOKENS` | 8192 | 预设中官方大压缩的保留量(token),在 `gate.js` 加载时读取。 |
237
+ | `SPINE_POINTER_TOKENS` | `SPINE_CHUNK_MAX`(默认 32000) | 单个工具结果超过此 token 数才只留指针。 |
238
+ | `SPINE_MIN_TOKENS` | 20000 | 轮后小切的最低上下文 token 数。 |
239
+ | `SPINE_KEEP_ROUNDS` | 3 | 原处保留的最近已结束工具轮数。 |
240
+ | `SPINE_KEEP_ROUND_TOKENS` | 32000 | 云端收尾的停止线;最近 3 个已结束工具轮始终留在原文。 |
241
+ | `SPINE_SHADOW_DIR` | `~/.dsh/compaction-spine/shadow` | 备份目录;设了 `DSH_HOME` 时以它替代 `~/.dsh`。 |
242
+ | `SPINE_SHADOW_MAX_BYTES` | 268435456 | 正数直接作为备份字节上限,优先于 `SPINE_SHADOW_MAX_MB`。 |
243
+ | `SPINE_SHADOW_MAX_MB` | 256 | 未设置正数字节上限时,按 MB 设置备份上限。 |
244
+ | `SPINE_ENGINE` | 自动识别 | 设为 `cloud` 或 `local` 可强制切法,其他值忽略。 |
245
+ | `SPINE_THUNDER` | 关闭 | 设为 `1` 才走雷霆。端侧不要开。 |
246
+ | `SPINE_THUNDER_MODEL` | `flash` | 设为 `27b` 且前缀超过 8 万时,每次官方压缩都少发。端侧不要设。 |
247
+ | `SPINE_ENGINE_FAIL` | 无 | **仅供测试**:用 `import`、`construct`、`after-super` 注入引擎故障。 |
248
+ | `SPINE_STOCK_FAIL` | 无 | **仅供测试**:设为 `1` 时禁止回退原版引擎。 |
249
+ | `SPINE_CHUNK_MIN` | 1000 | 一刀至少包含的 token 数。 |
250
+ | `SPINE_CHUNK_MAX` | 32000(`RAIL_TOKENS`) | 一刀最多包含的 token 数,也是指针阈值的默认值。 |
251
+ | `SPINE_COOLDOWN_MS` | 0 | 官方压缩后的额外时间窗口;仅正数开启,未设置、空串、非数字、0 或负数均关闭。 |
252
+
253
+ 每个新的 `compact-checkpoint` 只让轮后小切跳过一次,`spine-checkpoint` 不触发这个跳过。
254
+
255
+ `gate.js` 的官方压缩预设默认使用 `thresholdRatio: 0.8`、`headroomTokens: 65536`、`retainTokens: 8192`,可在启动前用对应环境变量覆盖。这些数值只管官方大压缩,不改小切切点。
256
+
180
257
  ## 安装
181
258
 
182
259
  从 npm 装。headless 和网页是两个 profile,要各装一次。桌面端是另一个 profile,普通 `dsh plugin --profile desktop` 会被拒绝。
183
260
 
184
261
  ```sh
185
- dsh plugin --profile headless add dsh-live-compact@1.0.8
186
- dsh plugin --profile web add dsh-live-compact@1.0.8
262
+ dsh plugin --profile headless add dsh-live-compact@1.1.0
263
+ dsh plugin --profile web add dsh-live-compact@1.1.0
187
264
  ```
188
265
 
189
266
  想先装到本机看版本:
@@ -193,12 +270,12 @@ npm install -g dsh-live-compact
193
270
  dsh-live-compact --version
194
271
  ```
195
272
 
196
- 备用:上不了 npm 的,用国内镜像站的包。1.0.2 到 1.0.7 的旧链接仍在。
273
+ 备用:上不了 npm 的,用国内镜像站的包。1.0.2 到 1.0.9 的旧链接仍在。
197
274
 
198
275
  ```sh
199
- dsh plugin --profile headless add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.8.tgz
200
- dsh plugin --profile web add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.8.tgz
201
- npm install -g --force "https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.0.8.tgz"
276
+ dsh plugin --profile headless add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.1.0.tgz
277
+ dsh plugin --profile web add https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.1.0.tgz
278
+ npm install -g --force "https://download.merkyorlynn.com/downloads/plugins/dsh-live-compact-1.1.0.tgz"
202
279
  dsh-live-compact --version
203
280
  ```
204
281
 
@@ -208,6 +285,8 @@ DSH-TUI 同样换不掉预设里的原版压缩,先不要装。
208
285
 
209
286
  被切掉的原文会写到 `~/.dsh/compaction-spine/shadow`,只有自己能读。上限 256MB。快满时删最老的备份。卡片里的序号是会话日志的事件号。要在当前会话的备份里找一行,用 `import { searchShadow } from "dsh-live-compact/shadow-search"`。
210
287
 
288
+ `dsh-live-compact stats` 的 `saved` 是切完当下少掉的 token,`rewrite` 是同一次收尾实际要重写的尾巴,只记一次。同一步连切不会按刀数把尾巴乘上去。这两个数口径不同,比值不是盈亏。桌面端没有终端,`rg "spine cut"` 只在终端输出里找得到。逐刀明细在 shadow 目录的 `spine-cuts.jsonl`。
289
+
211
290
  改上限:
212
291
 
213
292
  ```sh
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