dsh-gokifu-compact 1.7.1 → 1.8.1
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 +51 -0
- package/LICENSE +21 -0
- package/README.md +138 -203
- package/package.json +6 -7
- package/scripts/cli.mjs +46 -0
- package/scripts/install.mjs +283 -0
- package/src/engine.mjs +22 -2
- package/src/graph.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 1.8.1 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
### doctor:一天的 fold 收益一眼可见
|
|
6
|
+
|
|
7
|
+
- 新增「今天 fold 的总量」:`N 次(M 个会话)· 合计遮蔽 X tokens` —— 数据来自各会话棋谱的
|
|
8
|
+
`foldMarks`(按 fold 发生的 `at` 过滤,不按文件 mtime),不用翻会话日志
|
|
9
|
+
- 实测(2026-10-29):今天 26 次 fold / 12 个会话 / 遮蔽 **2,304,388 tokens**;重启后 **0 失败**
|
|
10
|
+
- 「上次 fold 的成本」继续显示真实 usage(含 provider 缓存字段:一次缓存命中的 fold 只有
|
|
11
|
+
5,329 新输入 + 647,168 缓存读,冷启动那次则是 629,184 全新输入)
|
|
12
|
+
|
|
13
|
+
## 1.8.0 — 2026-10-29
|
|
14
|
+
|
|
15
|
+
### 配置 id 统一 + 安装改成「两条命令、不用手改 YAML」
|
|
16
|
+
|
|
17
|
+
- **配置项 id 改名**:preset 里的 `- id: chess-context` → `- id: gokifu-compact`,
|
|
18
|
+
与展示名 `dsh-GOkifu-compact` 对齐(注入消息的 `source.kind` 仍是 `chess-context` ——
|
|
19
|
+
那是跨版本的数据契约,改了会让历史会话里的旧清单认不出来)
|
|
20
|
+
- **新增 CLI**:`dsh-gokifu-compact install | revert | doctor`
|
|
21
|
+
- `install` 自动找 profile/preset,把 preset 的 compaction group 里
|
|
22
|
+
`@deepseek-ai/dsh-compaction-basic` 那一行换成 `dsh-gokifu-compact`;
|
|
23
|
+
带备份(`.bak-before-gokifu-*`)、幂等、`--dry-run`、旧 id 自动纠偏
|
|
24
|
+
- `revert` 恢复最近一次备份;`doctor` 转发体检
|
|
25
|
+
- 逻辑只有一份(`scripts/install.mjs`),`test/install.mjs` 变成薄封装
|
|
26
|
+
- **不再声明 `dsh.bundle`**:本插件是「agent preset 内的压缩后端」,不是 profile 级 bundle。
|
|
27
|
+
声明 bundle 会让 `dsh plugin add` 把它塞进 `dsh.profile.bundles`,于是顶层再挂一份,
|
|
28
|
+
与 preset 内那份形成双实例(`ctx.compaction` 只能注册一次 ⇒ 构造期抛错)
|
|
29
|
+
- preset 示例默认写 `auto: true`:关掉原生 `compaction-basic` 不影响自动压缩 ——
|
|
30
|
+
本插件继承同一个 `BasicCompactionEngine`(同一套 between-step 压力触发与 context-overflow 兜底),
|
|
31
|
+
换的只是「压缩范围 = 上一个 checkpoint 之后」和「失败记忆逐字保留」
|
|
32
|
+
- `doctor` 新增两项检查:压缩后端已接管 / 原生已让位
|
|
33
|
+
- README 安装段重写为两步命令;新增 FAQ「关掉原生压缩,自动压缩还在吗?」
|
|
34
|
+
|
|
35
|
+
### 真机首折实测 + 三处收尾修复
|
|
36
|
+
|
|
37
|
+
- **GUI 里第一次真实 fold 成功**(重启后第一轮触发,`session-e16faf2b`):
|
|
38
|
+
遮蔽 seq 9–3167 / **440,847 tok** → 摘要 2 块 4.2K 字符(输入 629,184 / 输出 2,902 tok,`magpie/group/deepseek-v4.1-flash1017`);
|
|
39
|
+
surface 节点 1391 → 373,对话消息从 ~574K 降到 ~157K(面板 81% → 约 16%),热清单仍只有 1 条(就地替换生效)
|
|
40
|
+
- **修 `graph.checkpointSeq` 记错**:原来写「被遮蔽区间起点」(fold 后已不在 surface 上,实机一直是 9),
|
|
41
|
+
改成写「checkpoint 消息的 seq」
|
|
42
|
+
- **`doctor` 三处改进**:压缩统计改 **1 小时窗口 + 最近成功/失败时间**(历史失败不再吓人,
|
|
43
|
+
判定看「最新一条是不是成功」);新增「上次 fold 的成本(真实 usage)」;树新鲜度支持批量修复
|
|
44
|
+
- **`test/build-tree.mjs --all`**:一次性重建所有 index.md 落后于 graph.json 的会话(本次修了 48 个 / 1760 个节点文件)
|
|
45
|
+
- **摘要输出上限抬高**:`maxTokens` 默认 2048 → 4096,preset 3072 → **8192** ——
|
|
46
|
+
实测一次 fold 用掉 2,902 tok,3072 太贴边,触顶会 MAX_TOKENS 截断(截断 = fold 失败)
|
|
47
|
+
- **`graph.lastFoldUsage`**:每次 fold 的真实 usage(输入/输出/provider/model/时间)写进棋谱,计费对账不必再翻会话日志;
|
|
48
|
+
`doctor` 对新旧图都能显示(旧图没有该字段时从会话日志的 `compaction/summary` 事件兜底)
|
|
49
|
+
|
|
50
|
+
### 测试
|
|
51
|
+
|
|
52
|
+
118 项单测全绿 · `verify-install` 13 项全过(含 isolate group 内仍是我们的引擎行)
|
|
53
|
+
|
|
3
54
|
## 1.7.1 — 2026-10-29
|
|
4
55
|
|
|
5
56
|
### 发布后收尾
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MYCF711
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,261 +1,196 @@
|
|
|
1
1
|
# dsh-GOkifu-compact
|
|
2
2
|
|
|
3
|
-
> 已发布:npm `dsh-gokifu-compact@1.7.
|
|
4
|
-
> 安装:`dsh plugin --profile <name> add dsh-gokifu-compact`
|
|
3
|
+
> 已发布:npm [`dsh-gokifu-compact@1.7.1`](https://www.npmjs.com/package/dsh-gokifu-compact) · [GitHub Release v1.7.1](https://github.com/MYCF711/dsh-GOkifu-compact/releases/tag/v1.7.1)
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
**DSH 的棋谱式上下文压缩后端 —— 把「历史 → 一段摘要」换成「历史 → 可复盘的棋谱 DAG + 每步只有 323 tokens 的热清单」。**
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
```
|
|
8
|
+
热清单 323 tokens (2000 边压力测试;旧版全量渲染 28,800)
|
|
9
|
+
失败保留 8 / 8 (真实模型对照;官方 8 段模板 4.67 / 8)
|
|
10
|
+
清单开销 0.2% of prompt (v1.3.1 起;事故会话的旧版是 21.3%)
|
|
11
|
+
验证 118 项单测 · 真实包 8/8 · 真实进程 fold 3~6/6
|
|
12
|
+
```
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
GO = 围棋,kifu = 棋谱,compact = 压缩。一局棋下完,棋谱上不只有落子顺序,还有每一手的评语。
|
|
15
|
+
这个插件把 agent 的每一步变成一手棋(节点)、把当时的思考变成评语(目标 / 约束 / 失败原因),
|
|
16
|
+
整个任务变成一局可以复盘的棋。**棋谱是给模型用的,不是给人看的** —— 热清单里的每个字都为模型决策服务。
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|---|---|---|---|
|
|
15
|
-
| 本手 | 本手(ほんて) | honte / proper move | 成功路径(标准定式) |
|
|
16
|
-
| 恶手 | 悪手(あくしゅ) | aku / bad move | `[dead]` —— 没有成功路径经过 |
|
|
17
|
-
| 俗手 | 俗手(ぞくしゅ) | zokusu / vulgar move | `[redundant]` —— 表面正常,实际多走了 |
|
|
18
|
-
| 新手 | 新手(しんしゅ) | shinshu / new move | `[novel]` —— 探出新状态 |
|
|
19
|
-
| 缓手 | 緩手(かんしゅ) | kanshu / slow move | `[longer]` —— 绕远 |
|
|
20
|
-
| 手割 | 手割(てわり) | tewari | 手割分析(判断谁亏谁赚) |
|
|
21
|
-
| 胜负手 | 勝負手(しょうぶて) | shobute | 关键决策点(未实现) |
|
|
18
|
+
---
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
模型训练数据里这些词的出现频率比 `aku` / `zokusu` 高几个数量级,
|
|
25
|
-
标签是给模型**执行**的,不是给人**欣赏**的。术语表放这里(给人看设计理念)。
|
|
20
|
+
## 功能
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
- **增量 fold** —— 只压「上一个 checkpoint 之后」的新内容;一段历史一生只被摘要一次。
|
|
23
|
+
- **失败记忆** —— 每条失败尝试的意图 / 动作 / 报错原文 / seq 由规则抽取并**逐字**保留,模型没有丢弃它的权限。
|
|
24
|
+
- **手割(tewari)** —— 同一条失败边相对**每条**成功路径分别给结论:同型 / 绕路 / 探新,不是一个笼统的「失败」。
|
|
25
|
+
- **热清单** —— 每步重建的小清单:语义标记(`blocked:permission` / `dead retry-if:e1`)+ 动态图例 + 报错原文;**就地替换**,surface 上永远只有一条。
|
|
26
|
+
- **置换检测** —— 状态哈希命中历史节点时不新建分支,只在图上加一条边,并提示「回到节点 N —— 这里试过的路都记着」。
|
|
27
|
+
- **三层树** —— `index.md`(全量索引)/ `nodes/<id>.md`(单节点细节)/ `graph.json`(数据源),模型按需往下查。
|
|
28
|
+
- **压缩永不失败** —— 摘要三层降级:配置的摘要模型 → 当前路由模型 → 规则摘要(零模型调用),fold 一定能落地。
|
|
29
|
+
- **可诊断** —— `doctor` 一条命令体检、`measure-billing` 按真实 usage 对账、`rewash` 回洗历史误报。
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
## 怎么工作
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
```
|
|
34
|
+
每一步:事件流 ──► 棋谱 DAG(节点 = 状态;边 = 尝试 + 验证信号)
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
热清单(根目标 / 约束 / 成功路径 / 失败边)──► 注入模型
|
|
38
|
+
│
|
|
39
|
+
到阈值(默认窗口 45% 且增量 ≥ 40K)
|
|
40
|
+
▼
|
|
41
|
+
增量 fold:只压上一个 checkpoint 之后 ──► 棋谱摘要(失败边逐字保留)
|
|
42
|
+
```
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
索引让模型看到全部死路,节点给详情,原始补抽取的漏。
|
|
44
|
+
和官方 `dsh-compaction-basic` 的区别:
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
| | 官方 `compaction-basic` | 本插件 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| 压缩范围 | 从 surface 第一个非 system 节点起(**含上次 checkpoint**) | 上一个 checkpoint **之后** |
|
|
49
|
+
| 保留率 | 每轮重摘 → 按 ρ^k 衰减(实测安全规则 5 轮后剩 10%) | 一段内容只摘一次 |
|
|
50
|
+
| 失败记录 | 融进散文摘要,模型可以丢 | 规则抽取 + 逐字注入,模型无权丢 |
|
|
51
|
+
| 热上下文 | 无 | 323 tokens / 步,带语义标记与报错原文 |
|
|
41
52
|
|
|
53
|
+
### 热上下文长什么样
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
**「历史 → 棋谱 DAG(节点=状态,边=尝试+结果+验证信号) + 每步重建的极短热上下文」**。
|
|
55
|
+
每步 `agent/pre-step` 注入(内容不变时复用;变了就**就地替换**上一条,surface 永远只有一条):
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
```
|
|
58
|
+
<chess-ledger>
|
|
59
|
+
ROOT USER (verbatim): 把 tests/case*.test.js 跑绿:缓存键算错了。不要动 prod 配置。
|
|
60
|
+
CONSTRAINTS (verbatim):
|
|
61
|
+
- 不要动 prod 配置。
|
|
62
|
+
CURRENT GOAL: 缓存键修好,case7 通过
|
|
63
|
+
WORKING: user → n2 (exit 0)
|
|
64
|
+
LEARNED (3 failed attempts):
|
|
65
|
+
TRACE: <sessions>/<sid>/index.md (per-node detail: nodes/<id>.md)
|
|
66
|
+
# markers:
|
|
67
|
+
# dead = a working path avoids it — retry only if retry-if breaks
|
|
68
|
+
# redundant = working edge <edge> reaches the SAME position — take it
|
|
69
|
+
# blocked:permission = needs a permission this environment lacks — retry when it changes
|
|
70
|
+
- e2 "把缓存键改成 JSON.stringify 全部参数" → AssertionError: case7 expected 1 got 2 … [dead retry-if:e1]
|
|
71
|
+
- e3 "装全局 sqlite 依赖后再跑" → npm ERR! EACCES: permission denied, mkdir … [blocked:permission]
|
|
72
|
+
</chess-ledger>
|
|
73
|
+
```
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
早期约束与否定结果按 ρ^k 衰减(Compaction Cliff 实测:安全规则 5 轮后只剩 10%)。
|
|
51
|
-
2. **失败记忆是一等公民**:每条失败尝试(意图 / 动作 / 报错原文 / seq 坐标)由规则抽取,
|
|
52
|
-
**逐字**注入摘要 prompt,并在每步的热上下文里重建。模型没有「决定丢掉它」的权限。
|
|
53
|
-
3. **置换检测**:状态哈希(世界状态 + 已知约束集)命中历史节点时,不新建分支,只在图上加一条
|
|
54
|
-
指向旧节点的边,并提示「回到节点 N —— 这里试过的路都记着」。
|
|
75
|
+
预算不够时**降级不丢边**:先缩短意图与报错,再砍 seq,最后只列最近 N 条 + 汇总行。
|
|
76
|
+
标记档位可调:`semantic`(默认)| `terse`(只写三态)| `off`(不打标记)。
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
## 安装
|
|
57
79
|
|
|
58
|
-
|
|
80
|
+
**两步,不用手改任何 YAML。**
|
|
59
81
|
|
|
60
82
|
```bash
|
|
61
|
-
# 1)
|
|
62
|
-
|
|
83
|
+
# 1) 装包(DSH 会把它作为普通依赖装进 profile;不会把它塞进 bundles,避免双挂载)
|
|
84
|
+
dsh plugin --profile <profile> add dsh-gokifu-compact
|
|
63
85
|
|
|
64
|
-
# 2)
|
|
65
|
-
|
|
86
|
+
# 2) 一条命令接管压缩后端(自动改 preset 的 compaction group,带备份、可回滚、幂等)
|
|
87
|
+
npx dsh-gokifu-compact install --profile <profile>
|
|
66
88
|
|
|
67
|
-
# 3) 重启 DSH
|
|
89
|
+
# 3) 重启 DSH(插件在启动时加载),然后自检
|
|
90
|
+
npx dsh-gokifu-compact doctor
|
|
91
|
+
```
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
> 第 1 步会打印一条 `declares no dsh.bundle — installed as a plain dependency` —— **这是预期的**:
|
|
94
|
+
> 本插件挂在 agent preset 里,不做 profile 级 bundle(做了会在顶层再挂一份,与 preset 内那份双实例冲突)。
|
|
95
|
+
|
|
96
|
+
第 2 步做的是**唯一一件必须改文件的事**:把 agent preset 的 compaction group 里
|
|
97
|
+
`@deepseek-ai/dsh-compaction-basic` 那一行换成 `dsh-gokifu-compact`。为什么脚本必须改文件而不是
|
|
98
|
+
自动在启动时生效——因为那一行在 preset 的 `config.plugins` 数组内部,profile 级 / bundle 级的
|
|
99
|
+
patch 打不到它(`applyEntryPatches` 只递归 `group: true` 且 `config` 为数组的条目)。
|
|
100
|
+
脚本替你改、替你备份、也能替你还原:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx dsh-gokifu-compact install --dry-run # 先看会改什么(不写盘)
|
|
104
|
+
npx dsh-gokifu-compact revert # 还原(恢复 .bak-before-gokifu-* 备份)
|
|
71
105
|
```
|
|
72
106
|
|
|
73
|
-
|
|
107
|
+
**从源码(开发用,一条命令含链接 + 改 preset + 校验)**
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/MYCF711/dsh-GOkifu-compact.git
|
|
111
|
+
cd dsh-GOkifu-compact
|
|
112
|
+
node test/install.mjs --dry-run # 预演
|
|
113
|
+
node test/install.mjs # 真装
|
|
114
|
+
node test/rollback.mjs # 回滚
|
|
115
|
+
```
|
|
80
116
|
|
|
81
|
-
> ⚠
|
|
82
|
-
>
|
|
83
|
-
>
|
|
117
|
+
> ⚠ **压缩后端只能有一个**:`ctx.compaction` 在同一 isolate realm 里只能注册一次,同时挂
|
|
118
|
+
> `dsh-compaction-basic` 会在构造期抛 `service "compaction" has been registered`,并连累整个
|
|
119
|
+
> preset generation。安装器做的就是「换掉那一行」;不要手工再复制一份。
|
|
84
120
|
|
|
85
121
|
## 配置
|
|
86
122
|
|
|
87
123
|
```yaml
|
|
88
|
-
- id:
|
|
124
|
+
- id: gokifu-compact
|
|
89
125
|
name: 'dsh-gokifu-compact'
|
|
90
126
|
config:
|
|
127
|
+
auto: true # 自动压缩(继承基类的 between-step 压力触发)
|
|
91
128
|
thresholdRatio: 0.45 # 压力闸:min(窗口×0.45, 窗口−输出预留−64K)
|
|
92
|
-
headroomTokens: 65536
|
|
93
|
-
retainRatio: 0.16 # 逐字保留的近期尾部
|
|
94
|
-
retainTokens: # 或给绝对值(与 retainRatio 互斥)
|
|
95
129
|
growthFloorTokens: 40000 # 增长闸:距上次 fold 至少新增这么多 token
|
|
96
|
-
|
|
130
|
+
retainRatio: 0.16 # 逐字保留的近期尾部
|
|
97
131
|
summaryMode: hybrid # hybrid | rules(零模型调用)| llm
|
|
98
|
-
maxTokens:
|
|
99
|
-
summarizationProvider:
|
|
100
|
-
summarizationModel:
|
|
132
|
+
maxTokens: 8192 # 摘要输出上限(实测一次 fold 用到 2,902,留足余量防截断)
|
|
133
|
+
summarizationProvider: '' # 留空 = 跟随当前路由(最稳)
|
|
134
|
+
summarizationModel: '' # 想用免费模型,先确认 provider 的模型表里有那个 id
|
|
101
135
|
hotLedger: true # 每步注入失败清单
|
|
102
136
|
hotLedgerMaxTokens: 500
|
|
103
|
-
hotLedgerMarkers: semantic # semantic
|
|
104
|
-
hotLedgerReplace: true
|
|
105
|
-
transposition: true # 置换检测
|
|
106
|
-
tier2After: 8 # checkpoint 超过 8 个就把最老的两段合并
|
|
107
|
-
graphDir: '' # 留空 = <DSH_HOME>/chess-context
|
|
108
|
-
auto: true
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
`summaryMode` 的取舍:
|
|
112
|
-
|
|
113
|
-
| 模式 | 模型调用 | 适合 |
|
|
114
|
-
|---|---|---|
|
|
115
|
-
| `rules` | **0 次** | 高频小 fold、预算敏感;摘要由规则模板生成(目标/约束/死路/已验证/当前节点) |
|
|
116
|
-
| `hybrid`(默认) | 1 次/fold | 需要模型补写「意图」与「下一步」;硬事实仍由规则给定 |
|
|
117
|
-
| `llm` | 1 次/fold | 只在明确需要模型自由发挥时 |
|
|
118
|
-
|
|
119
|
-
## 推荐摘要模型
|
|
120
|
-
|
|
121
|
-
`summaryMode: hybrid` 需要一次模型调用。推荐把 `summarizationProvider` / `summarizationModel`
|
|
122
|
-
指向 **ima 免费模型**(先用 `magpie-ima-local-auth` 插件接入):
|
|
123
|
-
|
|
124
|
-
```yaml
|
|
125
|
-
summaryMode: hybrid
|
|
126
|
-
summarizationProvider: magpie
|
|
127
|
-
summarizationModel: ima/deepseek-v4.1-flash
|
|
137
|
+
hotLedgerMarkers: semantic # semantic | terse | off
|
|
138
|
+
hotLedgerReplace: true # 就地替换旧清单(surface 永远一条)
|
|
128
139
|
```
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
| 模板 | 死路保留 | 约束保留 | 摘要长度 |
|
|
133
|
-
|---|---|---|---|
|
|
134
|
-
| 本插件棋谱模板 + ima V4.1-Flash | **8/8(3/3 次)** | **2/2(3/3 次)** | 1,887 字符 |
|
|
135
|
-
| 官方 8 段模板 | 5/8 | 0/2 | ~3,072 字符 |
|
|
136
|
-
|
|
137
|
-
也就是说:**ima 这种免费小模型最适合的正是“单轮、结构化、无工具”的摘要任务**,而不是
|
|
138
|
-
当 DSH 的 agent 后端。它在这里比官方模板更听话,而且不占主路由额度。
|
|
139
|
-
|
|
140
|
-
证据:[evidence/ima-摘要测试.md](evidence/ima-摘要测试.md)(3 次采样原始输出)。
|
|
141
|
-
|
|
142
|
-
## 它产出什么
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
<DSH_HOME>/chess-context/sessions/<session>/graph.json 完整 DAG(节点 + 边 + 索引)
|
|
146
|
-
<DSH_HOME>/chess-context/sessions/<session>/graph.md 人读棋谱(节点/边/结果/置换标记)
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
DSH 自己的会话日志仍是原始记忆的唯一来源 —— 节点上只记 `seq` 坐标,需要逐字原文时按 seq 回读。
|
|
150
|
-
|
|
151
|
-
## 热上下文长什么样
|
|
152
|
-
|
|
153
|
-
每步 `agent/pre-step` 注入。**surface 上永远只有一条清单**:清单变了就**就地替换**上一条
|
|
154
|
-
(DSH 的 `surfaceOp:{op:"replace"}`,官方 system-prompt / tool-result-pruner 用的同一机制),
|
|
155
|
-
而不是再追加一条 —— 旧版不替换,实测一个会话累积 **176 条 / 81 万字符 ≈ 27 万 token**
|
|
156
|
-
(`evidence/P1-4-清单替换实测.md`)。
|
|
157
|
-
v1.5.0 之前追加出来的旧清单也会被**一次性换成一行占位**(surface 只支持 append/replace,不能删节点)——
|
|
158
|
-
实测事故会话的 surface 上还压着 44 条 / 29.5 万字符 ≈ 9.8 万 token,下次打开该会话时自动清掉。
|
|
159
|
-
|
|
160
|
-
**标记一定自带解释** ——
|
|
161
|
-
清单里出现的每个标记,图例里都有对应的一行(按实际用到的动态生成,不浪费预算):
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
<chess-ledger>
|
|
165
|
-
ROOT USER (verbatim): 把 tests/case*.test.js 跑绿:缓存键算错了。不要动 prod 配置。
|
|
166
|
-
CONSTRAINTS (verbatim):
|
|
167
|
-
- 不要动 prod 配置。
|
|
168
|
-
CURRENT GOAL: 缓存键修好,case7 通过
|
|
169
|
-
WORKING: user → n2 (exit 0)
|
|
170
|
-
LEARNED (3 failed attempts):
|
|
171
|
-
# markers:
|
|
172
|
-
# dead = a working path avoids it — retry only if that path breaks
|
|
173
|
-
# redundant = working edge <edge> reaches the SAME position — take that one
|
|
174
|
-
# blocked:permission = needs a permission this environment lacks — retry when it changes
|
|
175
|
-
- e2 "把缓存键改成 JSON.stringify 全部参数" → AssertionError: case7 expected 1 got 2 … [dead redundant:e1]
|
|
176
|
-
- e3 "装全局 sqlite 依赖后再跑" → npm ERR! EACCES: permission denied, mkdir … [blocked:permission]
|
|
177
|
-
</chess-ledger>
|
|
178
|
-
```
|
|
141
|
+
全部配置项与含义见 [`docs/性能与成本.md`](docs/性能与成本.md) 与 `src/engine.mjs` 的 `Config`。
|
|
179
142
|
|
|
180
|
-
|
|
143
|
+
## 常见问题
|
|
181
144
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
3. **原因(报错原文)任何档位都保留**:旧版 L3+ 把 `→ 原因` 整段砍掉,实测全库 133 条可见失败边里
|
|
187
|
-
只有 25 条带原因 —— 而模型判断「这是不是同一个坑」靠的就是报错原文。
|
|
188
|
-
4. **手割标记对最近 30 条边算**(每条一次 BFS):旧版「显示条数 ≤30 才算」⇒ 40 条失败边的会话
|
|
189
|
-
一条标记都没有,而模型只对最近几步做决策。
|
|
145
|
+
**关掉原生压缩,自动压缩还在吗?** 在 —— 本插件**就是**那个自动压缩后端:它继承官方的
|
|
146
|
+
`BasicCompactionEngine`(同一套 between-step 压力触发、context-overflow 兜底、事务语义),
|
|
147
|
+
`auto` 默认 `true`,装的不是「替代品」而是「换引擎」。变的只是「压缩范围 = 上一个 checkpoint 之后」
|
|
148
|
+
与「失败记忆逐字保留」。
|
|
190
149
|
|
|
191
|
-
|
|
192
|
-
|
|
150
|
+
**压缩之后数字会降吗?会不会只增不减?** 会降。一次 fold 用一个 checkpoint **替换掉整段被遮蔽的
|
|
151
|
+
历史**,tokenMeter 重新计算后数字直接下降;但不会归零 —— 还会保留逐字尾部(`retainRatio`,默认 16%)
|
|
152
|
+
和摘要本身。典型形状是**锯齿**:涨到阈值(窗口 45%)→ 压到约 20% → 再涨。如果你看到「只涨不降」,
|
|
153
|
+
先跑 `npx dsh-gokifu-compact doctor` 看压缩是不是在失败(`compaction/end` 的错误会直接列出来)。
|
|
193
154
|
|
|
194
|
-
|
|
155
|
+
**装完没反应 / 压缩没触发?** 插件在 DSH 启动时加载,**必须重启**。重启后跑 `node test/doctor.mjs`,
|
|
156
|
+
它会告诉你四件事:装没装上、摘要模型能不能解析、最近的压缩成功/失败、棋谱与树健不健康。
|
|
195
157
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
node test/doctor.mjs # 一条命令体检:安装 / 摘要模型 / 压缩成败 / 棋谱 / 树新鲜度
|
|
199
|
-
node test/verify-install.mjs # 13 项:安装与挂载校验(含运行期 graph.json 证据)
|
|
200
|
-
node test/verify-graphs.mjs # 全库棋谱健康检查(worldHash 类型 / stateIndex / renderFacts 不抛错)
|
|
201
|
-
node test/measure-billing.mjs # 真实 usage 计费审计(prompt / 缓存命中 / 清单占比 / fold 收支)
|
|
202
|
-
node test/measure-ledger.mjs # 热清单的 token 与信息量(--terse / --off 可换档对比)
|
|
203
|
-
node test/ab-render.mjs # A/B 夹具:同一张棋谱的三种标记档位各渲染一份(模型消费者实验用)
|
|
204
|
-
node test/rewash-graph.mjs # 历史棋谱回洗(dry-run 默认;--apply 写盘并重建树)
|
|
205
|
-
node test/real-headless-fold.mjs # **真实 dsh 进程**里触发 fold(headless profile + overlay;真 Session/真事务)
|
|
206
|
-
node test/evidence.mjs # 重算 evidence/模拟结果.md(计费、保留率、热上下文成本)
|
|
207
|
-
node test/bench.mjs 2000 # 2000 条边的性能与体量
|
|
208
|
-
|
|
209
|
-
# 用**真实的** @deepseek-ai 模块验证(从 app.asar 提取依赖闭包到 tmp,不重启 DSH)
|
|
210
|
-
node test/real-modules.mjs # import / 真 cordis 构造 / Config schema / 继承链
|
|
211
|
-
node test/real-fold.mjs # 真实 compactRegion 事务 + 棋谱摘要(rules 与 hybrid 都能跑通)
|
|
212
|
-
node test/real-fold-trigger.mjs # fold 自动触发(阈值/增长门/增量);需要真实模型,结果非确定,手动跑
|
|
213
|
-
|
|
214
|
-
# 用**真实模型**(magpie 默认路由组)验证
|
|
215
|
-
node test/real-model.mjs # 对照实验:本插件 8/8 vs 官方 4.67/8 失败边保留
|
|
216
|
-
node test/real-e2e.mjs # 真实 bash 失败 → 压缩 → 续跑 3/3
|
|
217
|
-
```
|
|
158
|
+
**会不会把失败记忆弄丢?** 不会。失败边由规则抽取并逐字注入摘要 prompt,模型输出后还会机械追加约束段;
|
|
159
|
+
模型没有「决定丢掉某条」的权限。真实模型对照里本插件保留 8/8,官方 8 段模板平均 4.67/8。
|
|
218
160
|
|
|
219
|
-
|
|
161
|
+
**摘要模型要配吗?** 不用,留空就跟随当前路由。配了但 provider 的模型表里没有那个 id 时,
|
|
162
|
+
引擎会退回路由模型;再不行就退回规则摘要(零模型调用)—— 宁可摘要朴素,也不让压缩失败。
|
|
220
163
|
|
|
221
|
-
|
|
164
|
+
**历史数据会不会把压缩弄坏?** 不会。插件加载棋谱时会自动修复旧格式(`worldHash` 对象 → 字符串、状态索引重建);
|
|
165
|
+
旧版本追加出来的几十条热清单会被一次性换成一行占位。全库 1,279 个坏字段实测归零。
|
|
222
166
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
| 系统注入被当成用户的话 | user 节点 13 个(含 Hindsight/系统说明) | **3 个**(全是真人输入) |
|
|
226
|
-
| 只读工具文本被当成失败 | failure 55 条(30+ 误报) | **8 条**(逐条核对全真) |
|
|
227
|
-
| 落盘滞后 | 每 25 事件才写 | 变化后 1.2s debounce 写 |
|
|
228
|
-
| 热上下文爆炸 | 1201 条边 → 28,800 tokens | **323 tokens**(L5 降级 + 汇总行 + 跳过装不下的档位) |
|
|
229
|
-
| 标记裸奔(v1.4.0) | `[blocked]` 出现却无解释;`redundant`/`longer` 从不在图例里 | 标记自带语义(`blocked:permission`)+ **按用到的标记动态生成图例** |
|
|
230
|
-
| 原因失踪(v1.4.0) | L3+ 把 `→ 报错原文` 整段砍掉(全库 133 条可见失败边只有 25 条带原因) | 任何档位都保留原因(最省档 25 字),带原因条数 89 → **209** |
|
|
167
|
+
**为什么压缩还是会让 prompt 缓存失效?** DSH 的 surface `replace` 语义决定:替换点之后的 KV 前缀会失效。
|
|
168
|
+
增量 fold 让替换点尽可能靠后,但无法消除(这是 seam 的语义,不是实现选择)。真实会话实测缓存命中率 94~97%。
|
|
231
169
|
|
|
232
|
-
|
|
170
|
+
**能和多分支/子代理一起用吗?** 子代理各自独立会话、各自一张棋谱;主会话的置换提示只在同一会话内生效。
|
|
233
171
|
|
|
234
|
-
|
|
235
|
-
|---|---|---|
|
|
236
|
-
| 纯函数 | `test/*.test.mjs` | 图不变量、判定规则、增量范围、热上下文降级 |
|
|
237
|
-
| 假 DSH | `test/engine-harness.test.mjs` | engine 的接线:构造 / 落账 / 注入 / 门控 / 溢出自救 |
|
|
238
|
-
| **真模块** | `test/real-modules.mjs` + `test/real-fold.mjs` | **重启后会不会崩**:依赖名对不对、能否继承官方事务、摘要是否真的替换了官方模板 |
|
|
172
|
+
## 参考源
|
|
239
173
|
|
|
240
|
-
|
|
241
|
-
再 import 的 —— 它验证的是真实类,不是 stub。
|
|
174
|
+
只借鉴**结构与规律**,未复制代码:
|
|
242
175
|
|
|
243
|
-
|
|
176
|
+
| 来源 | 用在哪 |
|
|
177
|
+
|---|---|
|
|
178
|
+
| [magpie-plugin-scaffold](https://github.com/MYCF711/magpie-plugin-scaffold)(MIT) | README 模板、发版七步、维护检查脚本(`scripts/`) |
|
|
179
|
+
| [billion-context](https://github.com/ranxianglei/billion-context)(MIT + 署名附加条款) | 「增量 fold / 只压已消费内容」的架构不变量、缓存与保留率的判据 |
|
|
180
|
+
| [opencode-dcp](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning)(MIT) | 「每个机制一节」的说明结构、对 prompt 缓存影响如实交代的写法 |
|
|
181
|
+
| [DSH 官方 `dsh-compaction` / `dsh-compaction-basic`](https://github.com/deepseek-ai)(随 DSH 分发) | 继承 `compactRegion` / `surfaceOp: replace` 事务语义,只覆盖策略层 |
|
|
244
182
|
|
|
245
|
-
|
|
246
|
-
但无法消除(这是 seam 的语义,不是实现选择)。
|
|
247
|
-
- **规则判定会误判**:判不出来的一律标 `unverified`,绝不标 `success`;`errorSnippet` 保留报错原文兜底。
|
|
248
|
-
- **不做多分支并行执行**:无廉价验证器时多分支搜索是烧钱(ReSCALE 反证),本插件只做「失败后换路」的记录与提示。
|
|
249
|
-
- **未在真实会话中重启验证过**(本会话不能重启自身):接线由 harness 测试覆盖,运行期表现按
|
|
250
|
-
`docs/重启后验证清单.md` 核对。
|
|
183
|
+
设计依据与实测数据在 [`docs/市场对标.md`](docs/市场对标.md) 与 [`evidence/`](evidence/)。
|
|
251
184
|
|
|
252
185
|
## 文档
|
|
253
186
|
|
|
254
|
-
- [方案书](docs/方案书.md) · [计划表](docs/计划表.md)
|
|
255
|
-
- [
|
|
256
|
-
- [
|
|
257
|
-
- [最终报告](docs/最终报告.md) · [
|
|
258
|
-
|
|
187
|
+
- [快速开始](QUICKSTART.md) · [方案书](docs/方案书.md) · [计划表](docs/计划表.md)
|
|
188
|
+
- [三层树设计](docs/三层树设计.md) · [DSH 技术测绘](docs/DSH-技术测绘.md)(1300 行,带 asar 行号)
|
|
189
|
+
- [性能与成本](docs/性能与成本.md) · [真实计费核对](evidence/真实计费核对.md)
|
|
190
|
+
- [最终报告](docs/最终报告.md) · [P0 修复记录](docs/P0-worldhash-死循环-根因与修复方案.md)
|
|
191
|
+
|
|
192
|
+
> 更新记录:[CHANGELOG.md](./CHANGELOG.md)(发版流程见 `scripts/release.mjs` 与 HANDOFF)
|
|
259
193
|
|
|
194
|
+
## 许可
|
|
260
195
|
|
|
261
|
-
|
|
196
|
+
MIT。见 [LICENSE](LICENSE)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gokifu-compact",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "DSH context-compression backend with a **go game record (kifu)** DAG: node = position, edge = attempted move + result. Incremental fold (each span summarized once), failure memory that survives compaction, transposition detection (chess-engine style), and a tiny hot ledger rebuilt every step.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/engine.mjs",
|
|
@@ -14,11 +14,6 @@
|
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=20"
|
|
16
16
|
},
|
|
17
|
-
"dsh": {
|
|
18
|
-
"bundle": {
|
|
19
|
-
"patch": "./cordis.patch.yml"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
17
|
"files": [
|
|
23
18
|
"src",
|
|
24
19
|
"!src/.engine-under-test.mjs",
|
|
@@ -27,7 +22,8 @@
|
|
|
27
22
|
"evidence",
|
|
28
23
|
"scripts",
|
|
29
24
|
"CHANGELOG.md",
|
|
30
|
-
"QUICKSTART.md"
|
|
25
|
+
"QUICKSTART.md",
|
|
26
|
+
"LICENSE"
|
|
31
27
|
],
|
|
32
28
|
"scripts": {
|
|
33
29
|
"test": "node test/run.mjs",
|
|
@@ -66,5 +62,8 @@
|
|
|
66
62
|
"@deepseek-ai/schemastery": {
|
|
67
63
|
"optional": false
|
|
68
64
|
}
|
|
65
|
+
},
|
|
66
|
+
"bin": {
|
|
67
|
+
"dsh-gokifu-compact": "./scripts/cli.mjs"
|
|
69
68
|
}
|
|
70
69
|
}
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-gokifu-compact —— 命令行入口
|
|
4
|
+
*
|
|
5
|
+
* dsh-gokifu-compact install [--profile desktop] [--preset personal-assistant] [--dry-run]
|
|
6
|
+
* dsh-gokifu-compact revert [--profile desktop]
|
|
7
|
+
* dsh-gokifu-compact doctor [--json]
|
|
8
|
+
*
|
|
9
|
+
* `install` 做的事只有一件是人手做起来最容易错的:把 agent preset 的 compaction group 里
|
|
10
|
+
* `@deepseek-ai/dsh-compaction-basic` 那一行换成 `dsh-gokifu-compact`(带备份、可回滚、幂等)。
|
|
11
|
+
* 为什么必须改 preset 文件而不是 bundle patch:见 scripts/install.mjs 顶部注释。
|
|
12
|
+
*/
|
|
13
|
+
import nodePath from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { run } from "./install.mjs";
|
|
17
|
+
|
|
18
|
+
const HERE = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const command = argv[0] ?? "";
|
|
21
|
+
|
|
22
|
+
if (command === "install" || command === "revert") {
|
|
23
|
+
process.exitCode = run(argv);
|
|
24
|
+
} else if (command === "doctor") {
|
|
25
|
+
const doctor = nodePath.join(HERE, "..", "test", "doctor.mjs");
|
|
26
|
+
try {
|
|
27
|
+
execFileSync(process.execPath, [doctor, ...argv.slice(1)], { stdio: "inherit" });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
process.exitCode = typeof error?.status === "number" ? error.status : 1;
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
console.log(`dsh-gokifu-compact — DSH 的棋谱式上下文压缩后端
|
|
33
|
+
|
|
34
|
+
用法:
|
|
35
|
+
dsh-gokifu-compact install [--profile <name>] [--preset <name>] [--dry-run]
|
|
36
|
+
把压缩后端从 dsh-compaction-basic 换成 dsh-gokifu-compact(自动改 preset,带备份,幂等)
|
|
37
|
+
dsh-gokifu-compact revert [--profile <name>]
|
|
38
|
+
还原上一次的 preset 备份
|
|
39
|
+
dsh-gokifu-compact doctor
|
|
40
|
+
体检:安装 / 摘要模型 / 压缩成败 / 棋谱 / 树新鲜度
|
|
41
|
+
|
|
42
|
+
装包(先做这一步):
|
|
43
|
+
dsh plugin --profile <name> add dsh-gokifu-compact
|
|
44
|
+
`);
|
|
45
|
+
process.exitCode = command === "" || command === "--help" || command === "-h" ? 0 : 1;
|
|
46
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install.mjs —— 一条命令让 dsh-gokifu-compact 接管压缩后端(不需要手改任何 YAML)
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须改 preset 文件:DSH 的压缩后端是挂在 **agent preset 的 compaction group 里**
|
|
5
|
+
* 的一行(`@deepseek-ai/dsh-compaction-basic`)。那是 preset 的 `config.plugins` 数组内部,
|
|
6
|
+
* 不是 loader 顶层条目 —— profile 级 / bundle 级的 patch 打不到它
|
|
7
|
+
* (`applyEntryPatches` 只递归 `group: true` 且 `config` 为数组的条目,而 preset 的 plugins
|
|
8
|
+
* 是 `config.plugins`;实测记录见 docs/DSH-技术测绘.md §7.6)。
|
|
9
|
+
*
|
|
10
|
+
* 这个脚本就是替你改那一行,并且:
|
|
11
|
+
* · 自动找 profile / preset(把 preset 里所有 `- id: compaction-basic` 块换掉)
|
|
12
|
+
* · 改之前备份(`*.bak-before-gokifu-<ts>`),`revert` 一键还原
|
|
13
|
+
* · `--dry-run` 只打印不写盘
|
|
14
|
+
* · 幂等:已经换过就跳过;旧版本残留(有我们的 name、id 还是旧名)只改名
|
|
15
|
+
* · 双模式:从仓库跑(会先建 junction + profile 依赖)/ 从已装的包跑(跳过链接)
|
|
16
|
+
*
|
|
17
|
+
* 用法:
|
|
18
|
+
* dsh-gokifu-compact install [--profile desktop] [--preset personal-assistant] [--dry-run]
|
|
19
|
+
* dsh-gokifu-compact revert [--profile desktop]
|
|
20
|
+
*
|
|
21
|
+
* 兼容:`node test/install.mjs` 仍然可用(薄封装到这个文件)。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import nodeFs from "node:fs";
|
|
25
|
+
import nodePath from "node:path";
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
export const PKG_NAME = "dsh-gokifu-compact";
|
|
30
|
+
export const ENTRY_ID = "gokifu-compact";
|
|
31
|
+
|
|
32
|
+
const HERE = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
export const PROJECT_DIR = nodePath.resolve(HERE, "..");
|
|
34
|
+
|
|
35
|
+
export function parseArgs(argv) {
|
|
36
|
+
const args = [...argv];
|
|
37
|
+
const command = args[0] && !args[0].startsWith("--") ? args.shift() : "install";
|
|
38
|
+
const get = (name, fallback) => {
|
|
39
|
+
const i = args.indexOf(`--${name}`);
|
|
40
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
command,
|
|
44
|
+
dryRun: args.includes("--dry-run"),
|
|
45
|
+
profile: get("profile", process.env.DSH_PROFILE ?? "desktop"),
|
|
46
|
+
preset: get("preset", ""),
|
|
47
|
+
dshHome: get(
|
|
48
|
+
"home",
|
|
49
|
+
process.env.DSH_HOME ?? nodePath.join(process.env.USERPROFILE ?? process.env.HOME ?? ".", "DeepSeek Harness", "home"),
|
|
50
|
+
),
|
|
51
|
+
log: args.includes("--quiet") ? () => {} : (...m) => console.log(...m),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function installedLocation(dshHome, profile) {
|
|
56
|
+
const profilePkg = nodePath.join(dshHome, "profiles", profile, "node_modules", PKG_NAME);
|
|
57
|
+
return nodePath.resolve(profilePkg) === PROJECT_DIR;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function presetsWithCompaction(dshHome) {
|
|
61
|
+
const root = nodePath.join(dshHome, "preset-bundles");
|
|
62
|
+
if (!nodeFs.existsSync(root)) return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const entry of nodeFs.readdirSync(root, { withFileTypes: true })) {
|
|
65
|
+
if (!entry.isDirectory()) continue;
|
|
66
|
+
const file = nodePath.join(root, entry.name, "cordis.patch.yml");
|
|
67
|
+
if (!nodeFs.existsSync(file)) continue;
|
|
68
|
+
const text = nodeFs.readFileSync(file, "utf8");
|
|
69
|
+
if (text.includes("- id: compaction-basic") || text.includes(PKG_NAME)) out.push({ name: entry.name, file, text });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 找到 `- id: compaction-basic` 那一块并整块替换(按缩进判断块边界)。
|
|
76
|
+
* @returns 新文本;找不到返回 null
|
|
77
|
+
*/
|
|
78
|
+
export function replaceCompactionBlock(text, blockLines) {
|
|
79
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
80
|
+
const startPattern = /^(\s*)- id: compaction-basic\s*$/;
|
|
81
|
+
let start = -1;
|
|
82
|
+
let foundIndent = "";
|
|
83
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
84
|
+
const m = startPattern.exec(lines[i]);
|
|
85
|
+
if (m) {
|
|
86
|
+
start = i;
|
|
87
|
+
foundIndent = m[1];
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (start < 0) return null;
|
|
92
|
+
let end = lines.length;
|
|
93
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
if (line.trim().length === 0) continue;
|
|
96
|
+
if (/^\s*#/.test(line)) continue;
|
|
97
|
+
const lead = (/^\s*/.exec(line)[0] ?? "").length;
|
|
98
|
+
if (lead === foundIndent.length && /^-\s/.test(line.trim())) {
|
|
99
|
+
end = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (lead < foundIndent.length) {
|
|
103
|
+
end = i;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const replacement = blockLines.map((line) => foundIndent + line);
|
|
108
|
+
return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 我们的引擎块(不含缩进,缩进由 replaceCompactionBlock 加)。 */
|
|
112
|
+
export function engineBlock() {
|
|
113
|
+
return [
|
|
114
|
+
`# ★ 压缩后端:棋谱式增量 fold(失败记忆 + 手割 + 置换检测)。`,
|
|
115
|
+
`# 替换掉原来的 @deepseek-ai/dsh-compaction-basic —— 两者会抢同一个 ctx.compaction。`,
|
|
116
|
+
`# 回滚:dsh-gokifu-compact revert (恢复 .bak-before-gokifu-* 备份)`,
|
|
117
|
+
`- id: ${ENTRY_ID}`,
|
|
118
|
+
` name: '${PKG_NAME}'`,
|
|
119
|
+
` config:`,
|
|
120
|
+
` auto: true # 自动压缩:继承基类的 between-step 压力触发`,
|
|
121
|
+
` thresholdRatio: 0.45 # 压力闸:min(窗口×0.45, 窗口−输出预留−64K)`,
|
|
122
|
+
` headroomTokens: 65536`,
|
|
123
|
+
` retainRatio: 0.16 # 逐字保留的近期尾部`,
|
|
124
|
+
` growthFloorTokens: 40000 # 增长闸:距上次 fold 至少新增这么多 token`,
|
|
125
|
+
` minFoldTokens: 8000`,
|
|
126
|
+
` summaryMode: hybrid # hybrid | rules(零模型调用)| llm`,
|
|
127
|
+
` summarizationProvider: '' # 留空 = 跟随当前路由`,
|
|
128
|
+
` summarizationModel: '' # 想用免费模型,先确认 provider 模型表里有那个 id`,
|
|
129
|
+
` hotLedger: true`,
|
|
130
|
+
` hotLedgerMaxTokens: 500`,
|
|
131
|
+
` hotLedgerMarkers: semantic`,
|
|
132
|
+
` hotLedgerReplace: true`,
|
|
133
|
+
` transposition: true`,
|
|
134
|
+
` tier2After: 8`,
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function backupFile(file, ts = Date.now()) {
|
|
139
|
+
const target = `${file}.bak-before-gokifu-${ts}`;
|
|
140
|
+
nodeFs.copyFileSync(file, target);
|
|
141
|
+
return target;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function revertPreset(file, log = console.log) {
|
|
145
|
+
const dir = nodePath.dirname(file);
|
|
146
|
+
const base = nodePath.basename(file);
|
|
147
|
+
const backups = nodeFs
|
|
148
|
+
.readdirSync(dir)
|
|
149
|
+
.filter((name) => name.startsWith(`${base}.bak-before-`))
|
|
150
|
+
.sort()
|
|
151
|
+
.reverse();
|
|
152
|
+
if (backups.length === 0) {
|
|
153
|
+
log(` ! ${file} 没有备份可恢复`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
nodeFs.copyFileSync(nodePath.join(dir, backups[0]), file);
|
|
157
|
+
log(` ✓ 已还原 ${file} ← ${backups[0]}`);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 旧版本残留:有我们的 name,但 id 还是别的(例如 chess-context)。只改 id 行。 */
|
|
162
|
+
function renameLegacyId(text) {
|
|
163
|
+
const lines = text.split("\n");
|
|
164
|
+
let changed = false;
|
|
165
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
166
|
+
const m = /^(\s*)- id:\s*([A-Za-z0-9._-]+)\s*$/.exec(lines[i]);
|
|
167
|
+
if (!m || m[2] === ENTRY_ID) continue;
|
|
168
|
+
const near = lines.slice(i, i + 3).join("\n");
|
|
169
|
+
if (!near.includes(PKG_NAME)) continue;
|
|
170
|
+
lines[i] = `${m[1]}- id: ${ENTRY_ID}`;
|
|
171
|
+
changed = true;
|
|
172
|
+
}
|
|
173
|
+
return changed ? lines.join("\n") : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function run(argv = process.argv.slice(2)) {
|
|
177
|
+
const opts = parseArgs(argv);
|
|
178
|
+
const { dryRun, profile, dshHome, log } = opts;
|
|
179
|
+
const profileDir = nodePath.join(dshHome, "profiles", profile);
|
|
180
|
+
|
|
181
|
+
if (!nodeFs.existsSync(profileDir)) {
|
|
182
|
+
console.error(`找不到 profile 目录:${profileDir}(用 --profile / --home 指定)`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const targets = presetsWithCompaction(dshHome).filter((p) => (opts.preset ? p.name === opts.preset : true));
|
|
187
|
+
if (targets.length === 0) {
|
|
188
|
+
console.error(`在 ${nodePath.join(dshHome, "preset-bundles")} 下没找到含 compaction-basic 的 preset`);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
log(`== dsh-gokifu-compact ${opts.command} ==`);
|
|
193
|
+
log(`profile: ${profileDir}`);
|
|
194
|
+
for (const t of targets) log(`preset : ${t.file}`);
|
|
195
|
+
log(dryRun ? "模式: dry-run(不写盘)\n" : "");
|
|
196
|
+
|
|
197
|
+
if (opts.command === "revert") {
|
|
198
|
+
for (const t of targets) revertPreset(t.file, log);
|
|
199
|
+
log("\n还原后重启 DSH 生效。");
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (opts.command !== "install") {
|
|
204
|
+
console.error(`未知命令:${opts.command}(可用:install / revert)`);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ① 确保包装进 profile(已装好则跳过) */
|
|
209
|
+
if (!installedLocation(dshHome, profile)) {
|
|
210
|
+
const dest = nodePath.join(profileDir, "node_modules", PKG_NAME);
|
|
211
|
+
if (nodeFs.existsSync(dest)) {
|
|
212
|
+
log(` · ${dest} 已存在(不是本目录),跳过链接`);
|
|
213
|
+
} else if (dryRun) {
|
|
214
|
+
log(` [dry] 会建 junction ${dest} → ${PROJECT_DIR}`);
|
|
215
|
+
} else {
|
|
216
|
+
nodeFs.mkdirSync(nodePath.dirname(dest), { recursive: true });
|
|
217
|
+
execFileSync("cmd", ["/c", "mklink", "/J", dest, PROJECT_DIR], { stdio: "ignore" });
|
|
218
|
+
log(` ✓ junction: ${dest} → ${PROJECT_DIR}`);
|
|
219
|
+
}
|
|
220
|
+
const pkgFile = nodePath.join(profileDir, "package.json");
|
|
221
|
+
const pkg = JSON.parse(nodeFs.readFileSync(pkgFile, "utf8"));
|
|
222
|
+
if (!pkg.dependencies?.[PKG_NAME]) {
|
|
223
|
+
if (dryRun) log(` [dry] 会在 package.json dependencies 加 ${PKG_NAME}`);
|
|
224
|
+
else {
|
|
225
|
+
nodeFs.copyFileSync(pkgFile, `${pkgFile}.bak-before-gokifu-${Date.now()}`);
|
|
226
|
+
pkg.dependencies = { ...(pkg.dependencies ?? {}), [PKG_NAME]: `link:${PROJECT_DIR.replace(/\\/g, "/")}` };
|
|
227
|
+
nodeFs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
228
|
+
log(" ✓ profile/package.json 已加 dependency");
|
|
229
|
+
}
|
|
230
|
+
} else log(` · profile/package.json 已有 ${PKG_NAME}`);
|
|
231
|
+
} else {
|
|
232
|
+
log(` · 已从 profile 的 node_modules 运行(安装态),跳过链接`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* ② 换 preset 里的压缩后端 */
|
|
236
|
+
for (const t of targets) {
|
|
237
|
+
const hasOurs = t.text.includes(PKG_NAME);
|
|
238
|
+
const hasOurId = /^\s*- id: gokifu-compact\s*$/m.test(t.text);
|
|
239
|
+
if (hasOurs && hasOurId) {
|
|
240
|
+
log(` · ${nodePath.basename(nodePath.dirname(t.file))}: 已是 ${ENTRY_ID},跳过`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (hasOurs && !hasOurId) {
|
|
244
|
+
const renamed = renameLegacyId(t.text);
|
|
245
|
+
if (renamed === null) {
|
|
246
|
+
log(` ! ${t.file}: 有 ${PKG_NAME} 但找不到可改的旧 id 行,跳过(请手工检查)`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (dryRun) {
|
|
250
|
+
log(` [dry] ${t.file}: 旧 id 改名为 ${ENTRY_ID}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const backup = backupFile(t.file);
|
|
254
|
+
nodeFs.writeFileSync(t.file, renamed, "utf8");
|
|
255
|
+
log(` ✓ ${t.file} 已把旧 id 改名为 ${ENTRY_ID}(备份 ${nodePath.basename(backup)})`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const next = replaceCompactionBlock(t.text, engineBlock());
|
|
259
|
+
if (next === null) {
|
|
260
|
+
log(` ! ${t.file}: 找不到 compaction-basic 块,跳过`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (dryRun) {
|
|
264
|
+
log(` [dry] ${t.file}: compaction-basic → ${ENTRY_ID}`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const backup = backupFile(t.file);
|
|
268
|
+
nodeFs.writeFileSync(t.file, next, "utf8");
|
|
269
|
+
log(` ✓ ${t.file} 已换后端(备份 ${nodePath.basename(backup)})`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (dryRun) {
|
|
273
|
+
log("\n这是预演,什么都没改。真装去掉 --dry-run。");
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
log("\n✓ 完成。重启 DSH 后生效(插件在启动时加载)。");
|
|
277
|
+
log(" 重启后自检:npx dsh-gokifu-compact doctor");
|
|
278
|
+
log(" 回滚:npx dsh-gokifu-compact revert");
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const invokedDirectly = process.argv[1] !== undefined && nodePath.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
283
|
+
if (invokedDirectly) process.exitCode = run();
|
package/src/engine.mjs
CHANGED
|
@@ -71,7 +71,7 @@ export const Config = z.object({
|
|
|
71
71
|
retainTokens: z.number().description("逐字保留的近期尾部(绝对 token);与 retainRatio 互斥"),
|
|
72
72
|
summarizationProvider: z.string().default("").description("摘要模型 provider;留空=跟随最近一次路由"),
|
|
73
73
|
summarizationModel: z.string().default("").description("摘要模型 model;留空=跟随最近一次路由"),
|
|
74
|
-
maxTokens: z.number().default(
|
|
74
|
+
maxTokens: z.number().default(4096).description("摘要输出上限"),
|
|
75
75
|
compactionRetries: z.number().default(1),
|
|
76
76
|
maxOverflowRetries: z.number().default(1),
|
|
77
77
|
modelPolicies: z.array(z.any()).default([]),
|
|
@@ -500,6 +500,13 @@ export default class ChessCompactionEngine extends BasicCompactionEngine {
|
|
|
500
500
|
## 约束(机械保留,逐字;不由模型改写)
|
|
501
501
|
${constraintsBlock.split("\n").map((l) => `- ${l}`).join("\n")}` });
|
|
502
502
|
}
|
|
503
|
+
// 记账:这次摘要调用了多少 token(provider 真实 usage)。#commit 会把它写进 graph.lastFoldUsage,
|
|
504
|
+
// doctor / 计费对账直接读,不用再翻会话日志里的 compaction/summary 事件。
|
|
505
|
+
if (assembler.usage !== undefined) {
|
|
506
|
+
this._lastSummaryUsage = assembler.usage;
|
|
507
|
+
this._lastSummaryProvider = used.provider;
|
|
508
|
+
this._lastSummaryModel = used.model;
|
|
509
|
+
}
|
|
503
510
|
return {
|
|
504
511
|
summary,
|
|
505
512
|
rawOutput: raw,
|
|
@@ -624,7 +631,20 @@ ${constraintsBlock.split("\n").map((l) => `- ${l}`).join("\n")}` });
|
|
|
624
631
|
const after = meter.measure(session);
|
|
625
632
|
g.foldCount += 1;
|
|
626
633
|
g.lastFoldTotalTokens = after.totalTokens;
|
|
627
|
-
|
|
634
|
+
// 记「checkpoint 消息的 seq」,不是被遮蔽区间的起点 —— 后者 fold 之后已不在 surface 上,
|
|
635
|
+
// 任何读这个字段做判断的地方都会拿到一个不存在的 seq(2026-10-29 实机:值一直是 9)。
|
|
636
|
+
g.checkpointSeq = checkpointSeqs(session).at(-1) ?? null;
|
|
637
|
+
if (this._lastSummaryUsage !== undefined) {
|
|
638
|
+
g.lastFoldUsage = {
|
|
639
|
+
...this._lastSummaryUsage,
|
|
640
|
+
provider: this._lastSummaryProvider ?? null,
|
|
641
|
+
model: this._lastSummaryModel ?? null,
|
|
642
|
+
at: Date.now(),
|
|
643
|
+
};
|
|
644
|
+
this._lastSummaryUsage = undefined;
|
|
645
|
+
this._lastSummaryProvider = undefined;
|
|
646
|
+
this._lastSummaryModel = undefined;
|
|
647
|
+
}
|
|
628
648
|
g.foldMarks.push({
|
|
629
649
|
at: Date.now(),
|
|
630
650
|
seqStart: range.start,
|
package/src/graph.mjs
CHANGED
|
@@ -64,6 +64,7 @@ export function createGraph({ sessionId, cwd = "" }) {
|
|
|
64
64
|
foldCount: 0,
|
|
65
65
|
lastFoldTotalTokens: 0,
|
|
66
66
|
foldMarks: [], // 每次 fold 的 {at, seqStart, seqEnd, nodeIds, edgeIds}
|
|
67
|
+
lastFoldUsage: null,// 最近一次摘要调用的真实 usage {inputTokens, outputTokens, provider, model, at}
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
|