dsh-engram 0.6.0 → 0.7.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/README.md +155 -39
- package/README.zh.md +119 -36
- package/cordis.patch.yml +18 -5
- package/docs/CONTEXT-GC-GUIDE.zh.md +35 -6
- package/docs/PROPOSAL-evolution.zh.md +25 -1
- package/docs/RELEASE-NOTES-v0.7.0.md +57 -0
- package/docs/RELEASE-NOTES-v0.7.1.md +88 -0
- package/lib/anchor.js +124 -0
- package/lib/boundary-prune.js +27 -0
- package/lib/client.js +7 -2
- package/lib/compaction.js +11 -0
- package/lib/context-gc.js +47 -0
- package/lib/entity-page.js +200 -0
- package/lib/episode.js +163 -0
- package/lib/index-block.js +74 -4
- package/lib/index.js +136 -9
- package/lib/settings.js +6 -0
- package/lib/store.js +48 -5
- package/lib/tools.js +25 -3
- package/lib/web-provision.js +324 -91
- package/package.json +1 -1
- package/scripts/dsh-engram.mjs +196 -53
|
@@ -32,6 +32,13 @@ dsh-engram 的 Context GC 把这一步换成:
|
|
|
32
32
|
| host 平面 | headless / TUI / base 型 profile | 打开 `gcReplacesCompaction` 后,插件启动时注册 ContextGcEngine;否则挂裸 `BasicCompactionEngine`(宿主摘要) |
|
|
33
33
|
| web 平面 | dsh web 的 agent preset(shipped + 用户预设) | 打开 `autoWebCompaction` 后,启动时把 stock 预设的 `compaction-basic` 行换成 `dsh-engram/compaction` 行 |
|
|
34
34
|
|
|
35
|
+
web 平面在两种 DSH 预设布局上都生效(插件按 `compaction` 组的结构定位行,不依赖缩进):
|
|
36
|
+
|
|
37
|
+
- **DSH ≤ 0.1.x:旧版组合文件** `<root>/<id>/agent.cordis.yml`(行在 4 空格缩进);
|
|
38
|
+
- **DSH ≥ 0.2.0:声明式出厂预设** `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml`
|
|
39
|
+
(行在 14/16/18 空格缩进,文件里是 `- insert:` 声明的 `@deepseek-ai/dsh-agent-preset`)。
|
|
40
|
+
用户自定义的声明式预设(存在 profile patch 里、没有出厂文件)不会被改写。
|
|
41
|
+
|
|
35
42
|
---
|
|
36
43
|
|
|
37
44
|
## 2. 前提
|
|
@@ -57,7 +64,9 @@ cd /path/to/dsh-loom && npm link # 或 profile package.json 加 "dsh-
|
|
|
57
64
|
# ③ 启动一次 dsh web —— 这一步做「自动装配」
|
|
58
65
|
# - host 平面:按 gcReplacesCompaction 注册 Context GC 为 compaction 服务
|
|
59
66
|
# - web 平面:改写每个 stock 预设文件(compaction-basic 行 → dsh-engram/compaction 行,
|
|
60
|
-
# 每个文件旁留
|
|
67
|
+
# 每个文件旁留 .engram.bak 备份):
|
|
68
|
+
# DSH ≤ 0.1.x → <root>/<id>/agent.cordis.yml.engram.bak
|
|
69
|
+
# DSH ≥ 0.2.0 → <presets>/<id>.patch.yml.engram.bak
|
|
61
70
|
dsh web
|
|
62
71
|
|
|
63
72
|
# ④ 再重启一次 dsh web —— 让已接管的预设真正生效
|
|
@@ -144,8 +153,8 @@ Fetch the exact records that are still in the store:
|
|
|
144
153
|
|
|
145
154
|
| 键 | 默认 | 作用 |
|
|
146
155
|
|---|---|---|
|
|
147
|
-
| `autoWebCompaction` | `
|
|
148
|
-
| `gcReplacesCompaction` | `
|
|
156
|
+
| `autoWebCompaction` | `false` | 是否在启动时自动改写 stock 预设并接管 web 平面(opt-in,见开头) |
|
|
157
|
+
| `gcReplacesCompaction` | `false` | `true`=用 Context GC;`false`=回退 DSH 默认 LLM 摘要(`compaction` 服务永不缺席)。opt-in |
|
|
149
158
|
| `gcNarrative` | `true` | 无锚轮次是否走 scoped LLM 叙事;`false`=**纯机械、整条路径零 LLM** |
|
|
150
159
|
| `gcNarrativeMaxTokens` | `1024` | 叙事生成的最大 token 数 |
|
|
151
160
|
| `gcNarrativeMaxChars` | `4000` | 无锚轮次原文兜底的截断长度(字符) |
|
|
@@ -158,13 +167,15 @@ Fetch the exact records that are still in the store:
|
|
|
158
167
|
|
|
159
168
|
| 命令 | 作用 |
|
|
160
169
|
|---|---|
|
|
161
|
-
| `npx dsh-engram status` | 查看 host +
|
|
170
|
+
| `npx dsh-engram status` | 查看 host + 每个预设的接管状态与生效配置(自动适配新旧预设布局) |
|
|
162
171
|
| `npx dsh-engram doctor` | status + 按缺口排序的下一步 |
|
|
163
172
|
| `npx dsh-engram enable` | 手动重跑启动时的自动装配(通常 no-op) |
|
|
164
173
|
| `npx dsh-engram revert` | 把全部预设还原成 stock `compaction-basic` 行 |
|
|
174
|
+
| `npx dsh-engram status --presets-dir <dir>` | 声明式预设目录探测失败时手动指定(如 `DSH_WEB_PRESETS_DIR`) |
|
|
165
175
|
|
|
166
176
|
- 仓库内 `npm run web-compaction:*` 是同一 CLI 的别名。
|
|
167
|
-
- 每个被改写文件旁有
|
|
177
|
+
- 每个被改写文件旁有 `.engram.bak`(create-only,保留首次原件):
|
|
178
|
+
`agent.cordis.yml.engram.bak`(旧版布局)或 `<id>.patch.yml.engram.bak`(声明式布局)。
|
|
168
179
|
- **卸载 dsh-engram 之前先 `npx dsh-engram revert`**——否则预设里的 `dsh-engram/compaction` 行会
|
|
169
180
|
悬空,web 会话挂 loading。
|
|
170
181
|
|
|
@@ -179,13 +190,31 @@ Fetch the exact records that are still in the store:
|
|
|
179
190
|
| 长会话自动压缩仍不提交(`start` 涨、`summary` 不涨) | 同上 / 或某些后端版本旧 | 重启;确认日志有 `compaction = Context GC` |
|
|
180
191
|
| 日志 `… keeping the existing compaction service` | 同域已有 compaction provider(基座行未禁 / 双 provider) | 检查 patch 与预设行,避免重复注册 |
|
|
181
192
|
| 日志 `web-provision … skipped (custom)` | 该预设是自定义/无 compaction 布局 | **设计如此**,不会碰 |
|
|
193
|
+
| 日志 `preset "x" has no shipped file …` | DSH ≥ 0.2.0 的声明式用户预设(存在 profile patch,无出厂文件) | 在设置卡/编辑器里改;或手动把 compaction 组换成 `dsh-engram/compaction` |
|
|
194
|
+
| 改了 shipped 预设文件却不生效 | profile patch 里有 `preset-*` 覆写行(Web 编辑器保存的 `config.plugins` 优先) | 在编辑器里改,或删掉覆写行 |
|
|
182
195
|
| 日志 `dsh-compaction-basic unavailable` | 环境缺后端依赖 | 安装依赖;自动回退默认压缩,host 不崩 |
|
|
183
196
|
| 极小被驱逐片段(span 小于 checkpoint 开销)不压缩 | harness 收缩闸门对**任何**后端一视同仁 | 属正常,没必要为几十 token 起 checkpoint |
|
|
184
197
|
| 改配置后没变化 | web 行要重启才刷新 | 重启 dsh web |
|
|
198
|
+
| 遥测「节省 · 剪枝/压缩」一直是 0、净 token 恒负 | 两条入账腿都没触发:L1 在 0.2.0 web profile 下拿不到 host 平面的 `toolResultPruner`;压缩节省此前进不了账本 | 确认状态快照 `boundaryPrune.armed: true`(`npx dsh-engram status` 会打印);升级到 0.7.1+ 后压缩会按 `区间字符数 − checkpoint 字符数` 入账 |
|
|
185
199
|
|
|
186
200
|
---
|
|
187
201
|
|
|
188
|
-
## 9.
|
|
202
|
+
## 9. 账本口径(为什么净 token 可能为负)
|
|
203
|
+
|
|
204
|
+
`net = avoided − injected − rewriteChars × (1 − ledgerCacheDiscount)`,三项都按
|
|
205
|
+
`chars / 4` 估算(见 README「Context Ledger」):
|
|
206
|
+
|
|
207
|
+
- **injected**:`[ENGRAM]`/`[RECALL]`/`[JIT]`/`[ESR]` 块每请求重计的注入成本;
|
|
208
|
+
- **avoided**:**已入账**的节省,只有两条腿——L1 边界剪枝(`toolResultPruner` 剪掉的
|
|
209
|
+
chars × 后续请求数)与压缩(每次 checkpoint 的「被替换区间字符数 − checkpoint 字符数」× 后续请求数);
|
|
210
|
+
- **rewrite penalty**:L1 剪枝会击穿 KV 前缀缓存,按 `1 − 缓存折价` 一次性计价;
|
|
211
|
+
压缩**不额外计**罚金(压缩是宿主自己的动作,默认后端同样会改写前缀,计了会重复)。
|
|
212
|
+
|
|
213
|
+
所以:**没剪过、没压缩过的会话,节省必然是 0,净值必然是负的**——负数不代表账本坏了,
|
|
214
|
+
只代表这一段时间只花了钱没省到。状态快照的 `boundaryPrune.armed` 告诉你 L1 是否真的
|
|
215
|
+
武装;压缩入账要等第一次 checkpoint。
|
|
216
|
+
|
|
217
|
+
## 10. FAQ
|
|
189
218
|
|
|
190
219
|
- **有损吗?** 锚定细节**无损**(全部可通过工具重取);只有无锚轮次走叙事/截断;所有原文仍留在
|
|
191
220
|
会话日志里,即使 checkpoint 被裁剪也能找回。
|
|
@@ -343,7 +343,7 @@ net : avoided − injected − rewrites×(1−cacheDiscount)
|
|
|
343
343
|
- **状态**:已实现(2026-10-19)——`lib/ledger.js`(纯函数 + 原子落盘 + delta flush)、
|
|
344
344
|
`test/ledger.test.mjs` 12 例、`/stats` 与 `/overview` 的 `ledger` 字段、
|
|
345
345
|
`ledgerEnabled` / `ledgerCacheDiscount` 两个设置键、GUI 遥测页账本面板 +
|
|
346
|
-
概览「上下文净 token」卡;`npm test`
|
|
346
|
+
概览「上下文净 token」卡;`npm test` 381/381。
|
|
347
347
|
- 反事实基线:同一会话关闭机制时的 avoided 由 oracle 离线算,账本只报「实际发生」。
|
|
348
348
|
|
|
349
349
|
**B. 自适应默认(`boundaryPrune: "auto"`)**
|
|
@@ -424,6 +424,13 @@ conflicts : contradicts 边 + 待裁决标记
|
|
|
424
424
|
- 注入时只给**实体卡**(≤3 行:truth 首行 + open loops 数 + 更新时间),
|
|
425
425
|
详情走 `engram_detail`/页——继续渐进披露。
|
|
426
426
|
- 索引从「平铺行」升级为「实体卡 + 少量独立事实」,同样 700c 预算内信息密度更高。
|
|
427
|
+
- **状态(2026-10-19)**:**B 已实现**——`lib/entity-page.js`
|
|
428
|
+
(`buildEntityPage` / `resolveEntity` / `renderEntityCard` / `renderEntityPage`);
|
|
429
|
+
`selectEntityCards` 在 `[ENGRAM]` 前置最多 2 张实体卡(truth 首条 + mem/open 计数 +
|
|
430
|
+
`engram_detail(entity=…)` 指针),并把这些实体的平铺记忆行从索引中去重;
|
|
431
|
+
`engram_detail` 新增可选 `entity` 参数返回完整页(truth/timeline/open loops/
|
|
432
|
+
conflicts/relations);`entityCardsEnabled` 默认开。`test/entity-page.test.mjs` 7 例。
|
|
433
|
+
**C(锚点重验证 / 分层 TTL / 巩固任务)仍未做**。
|
|
427
434
|
|
|
428
435
|
**C. 巩固任务(可离线)**
|
|
429
436
|
|
|
@@ -438,12 +445,29 @@ conflicts : contradicts 边 + 待裁决标记
|
|
|
438
445
|
召回/索引前抽查;锚点失效 → 自动降权并标 `stale`,而不是继续当真相。
|
|
439
446
|
- `expireDays` 从「整条过期」改为「分层 TTL」:episode 短、fact 长、preference 永久。
|
|
440
447
|
|
|
448
|
+
- **状态(2026-10-19)**:**C 的锚点 + 分层 TTL 已实现**——`lib/anchor.js`
|
|
449
|
+
(`makeFileAnchor` / `anchorStatus` / `memoryIsStale` / `collectStaleAnchors`),
|
|
450
|
+
记忆行新增 `anchor` 字段(path/mtimeMs/size/hash?/at;≤256KB 存 sha256);
|
|
451
|
+
会话启动时有界重验证(≤40 条),改动/缺失 → 打 `stale` 标签,索引与实体 truth
|
|
452
|
+
不再展示、timeline 标 `· stale`、`engram_detail` 仍可重取;分层 TTL:
|
|
453
|
+
preference/procedure 永久、episode 14d、error 60d、其余 expireDays。
|
|
454
|
+
`test/anchor.test.mjs` 6 例。**离线巩固任务(episode 抽象/合并)未做**:
|
|
455
|
+
纯规则只能 rollup、不是语义抽象,在没有证据前不引入(与 v0.8 playbook 一起考虑)。
|
|
456
|
+
|
|
441
457
|
**D. Resume Brief(隔天继续)**
|
|
442
458
|
|
|
443
459
|
打开旧会话/同 workspace 新会话时,注入 ≤5 行:上次停在哪、未闭环任务、
|
|
444
460
|
最近的失败与修复、下一步建议。这是本项目对「73% 请求在长会话」最直接的产品回答,
|
|
445
461
|
也是与宿主 checkpoint 的差异化(宿主存状态,我们存**意义**)。
|
|
446
462
|
|
|
463
|
+
- **状态(2026-10-19)**:**A + D 已实现**——`lib/episode.js` 的 `buildEpisode()`
|
|
464
|
+
在 `session/disposed` 生成零 LLM 纪要(文件/命令/失败→修复/决策/未闭环,仅实质会话写入,
|
|
465
|
+
永不进索引,每 workspace 保留 20 条);`renderResumeBrief()` 在新会话注入最近 7 天
|
|
466
|
+
最新 episode(`[RESUME]` ≤240 字符 + `engram_detail` 指针);设置键
|
|
467
|
+
`episodeEnabled` / `resumeBriefEnabled`;`test/episode.test.mjs` 9 例。
|
|
468
|
+
**B(实体页)/ C(锚点重验证、分层 TTL)待做**——它们需要 schema 迁移与索引渲染改造,
|
|
469
|
+
留到下一轮。
|
|
470
|
+
|
|
447
471
|
**门:**
|
|
448
472
|
|
|
449
473
|
- 跨会话问答集(自建 40 题 + LongMemEval 子集)Hit@3 提升 ≥ 20%;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
## dsh-engram v0.7.0
|
|
2
|
+
|
|
3
|
+
自 v0.6.0 以来:把「记忆」从**事件记录**推进到**可维护的当前真相**——会话结束自动沉淀
|
|
4
|
+
episode、新会话给恢复简报、`esr_node` 升级为实体页、文件锚点让过期真相可被检测。
|
|
5
|
+
|
|
6
|
+
### 功能
|
|
7
|
+
|
|
8
|
+
- **会话 episode(v0.7-A)** — `session/disposed` 时用零 LLM 规则把事件日志压成一条紧凑
|
|
9
|
+
纪要:改过哪些文件、跑过哪些命令、哪些失败后修复(`cmd×N→fixed`)、git commit 主题、
|
|
10
|
+
仍挂着的 todo。只在有实质内容时写(纯 `ls` 会话不写),**永不进 `[ENGRAM]` 索引**,
|
|
11
|
+
每个 workspace 保留最近 20 条。
|
|
12
|
+
- **恢复简报(v0.7-D)** — 新会话第一次组装时,取最近 7 天最新的 episode,注入
|
|
13
|
+
≤240 字符的 `[RESUME]` 行 + `engram_detail` 指针;与 `[ENGRAM]`/`[RECALL]` 一起按会话冻结。
|
|
14
|
+
没有近期 episode 就不注入。
|
|
15
|
+
- **实体页 + 实体卡(v0.7-B)** — `esr_node` 升级为可读页面:`truth`(preference 优先、
|
|
16
|
+
superseded 剔除)/ `timeline` / `open loops`(active 任务 + 证据缺口)/ `conflicts`
|
|
17
|
+
(无序对去重)/ `relations`。`[ENGRAM]` 为活跃实体前置最多 2 张一行卡
|
|
18
|
+
(truth 首条 + mem/open + 下钻指针),被代表的实体不再重复平铺行;
|
|
19
|
+
`engram_detail(entity="<name|id>")` 返回完整页。
|
|
20
|
+
- **可重验证锚点(v0.7-C)** — 带 `filePath` 的记忆在写入时记录
|
|
21
|
+
`{path, mtimeMs, size, hash?}`(≤256KB 存 sha256);会话启动时有界重验证(≤40 条),
|
|
22
|
+
文件改动/缺失 → 打 `stale` 标签,索引与实体 truth 不再展示,timeline 标注 `· stale`,
|
|
23
|
+
`engram_detail` 仍可重取(不删除)。
|
|
24
|
+
- **分层 TTL(v0.7-C)** — `preference` / `procedure` 永久;`episode` 14 天;`error` 60 天;
|
|
25
|
+
其余按 `expireDays`(默认 180 天)。仍然只有 GC 归档、没有硬删。
|
|
26
|
+
|
|
27
|
+
### 兼容性说明
|
|
28
|
+
|
|
29
|
+
- 存储 schema 只**新增**字段:`memories.anchor`(nullable,默认 null)与新的 memory kind
|
|
30
|
+
`episode`。旧数据读取自动补默认值,无需迁移;真实实例冒烟用 256 条既有记忆验证通过。
|
|
31
|
+
- 只有**新写入**且带 `filePath` 的记忆会落锚;既有记忆不会被回溯锚定(避免一次性把历史
|
|
32
|
+
事实全部标 stale)。需要时可后续加显式 backfill 命令。
|
|
33
|
+
- 新增设置键(均默认开):`episodeEnabled`、`resumeBriefEnabled`、`entityCardsEnabled`、
|
|
34
|
+
`anchorRevalidate`;隐藏可调 `episodeMaxPerWorkspace`、`resumeBriefMaxAgeDays`、
|
|
35
|
+
`resumeBriefMaxChars`、`entityCardsMax`、`entityCardChars`、`episodeTtlDays`、
|
|
36
|
+
`errorTtlDays`。GUI 旋钮总数 34。
|
|
37
|
+
- 升级后需重启 `dsh web`;episode 要等一个会话结束、resume brief 在下一个新会话生效。
|
|
38
|
+
|
|
39
|
+
### 其他
|
|
40
|
+
|
|
41
|
+
- 测试 **381/381**(新增 episode 9 例、entity 7 例、anchor 6 例);`npm run eval` BENCH PASS;
|
|
42
|
+
`npm run typecheck` 通过;`lib/client.js` 重建。
|
|
43
|
+
- 文档:中英 README(episode / 实体页 / 锚点三节)、`docs/PROPOSAL-evolution.zh.md`
|
|
44
|
+
(v0.7 A/B/C/D 状态)、设置卡新增「会话纪要 / 恢复简报 / 实体卡」开关。
|
|
45
|
+
|
|
46
|
+
## DSH 版本支持
|
|
47
|
+
|
|
48
|
+
- **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
|
|
49
|
+
- **peer 声明**:
|
|
50
|
+
- `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
51
|
+
- `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
52
|
+
- `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
53
|
+
- `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
54
|
+
- `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
55
|
+
- **验证版本**:`dsh 0.2.0-rc.2`
|
|
56
|
+
- **最后验证日期**:2026-10-19
|
|
57
|
+
- **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
## dsh-engram v0.7.1
|
|
2
|
+
|
|
3
|
+
自 v0.7.0 以来:**修复 DSH `0.2.0` 声明式预设下的 web 平面 Context GC 接管**。v0.7.0(及
|
|
4
|
+
更早)的自动装配以旧版 `agent.cordis.yml` 的布局为锚(`compaction-basic` 行固定 4 空格缩进),
|
|
5
|
+
而 `0.2.0` 把代理预设换成了 `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml` 里的
|
|
6
|
+
`@deepseek-ai/dsh-agent-preset` 声明(行在 14/16/18 空格缩进)——结果是 **autoWebCompaction
|
|
7
|
+
静默跳过所有预设**,且旧的字符串替换不含行尾换行,极端情况下会半改写 stock 行。
|
|
8
|
+
|
|
9
|
+
### 修复
|
|
10
|
+
|
|
11
|
+
- **结构化定位 compaction 行** — 不再按缩进/整行字面量匹配:先找 YAML 结构里的
|
|
12
|
+
`compaction` 组(带 `group: true` 的列表项),再在组内按 `- id:` **且** `name:` 双重校验
|
|
13
|
+
定位 stock / engram 行,替换块继承该行自己的缩进。旧版组合文件(4 空格)与声明式补丁文件
|
|
14
|
+
(14/16/18 空格)都逐字节往返一致(swap → restore 还原原件)。
|
|
15
|
+
- **声明式预设发现** — DSH `0.2.0` 的预设 roster 不再携带 `path`。插件把 roster 的每个 id
|
|
16
|
+
映射到出厂的 `<presetsDir>/<id>.patch.yml`(经 dsh 安装解析,`DSH_WEB_PRESETS_DIR` 可覆盖),
|
|
17
|
+
找不到出厂文件(= 用户自定义声明式预设 / profile patch 覆写)时跳过并打明确日志,不再当成故障。
|
|
18
|
+
- **删除一个潜在的半改写 bug** — stock 替换原用不含行尾的固定字符串,若文件恰好把
|
|
19
|
+
`compaction-basic` 行与下一行同缩进紧挨着,替换会产生缺换行的坏文件;新实现按整块替换。
|
|
20
|
+
- **状态 / doctor CLI 适配两种布局** — `npx dsh-engram status|doctor` 自动发现旧版
|
|
21
|
+
`agent.cordis.yml` 与声明式 `*.patch.yml`,显示检测到的布局;doctor 新增
|
|
22
|
+
`autoWebCompaction=false`(opt-in 未打开,web 平面必然 no-op)、node_modules 预设(`npm update dsh`
|
|
23
|
+
会恢复出厂布局,仅 autoWebCompaction 保持开启时下次启动自动重接管)、profile patch 里
|
|
24
|
+
`preset-*` 覆写行(Web 编辑器保存的 `config.plugins` 优先于 shipped 文件)的三类提示。
|
|
25
|
+
`enable` 手动重跑时镜像状态快照里记录的 `gcReplacesCompaction`/`gcNarrative`,不会把手动
|
|
26
|
+
enable 的预设行翻成与设置卡不一致的开关。
|
|
27
|
+
- **状态快照不再假装「启动中」** — `autoWebCompaction=false` 时 web 平面记为 `disabled`
|
|
28
|
+
(此前恒为 `pending`,`dsh-engram status` 会误显示「启动中(agentPresets 尚未就绪)」),
|
|
29
|
+
CLI 增加「未启用(autoWebCompaction=false)」文案并保留 opt-in 警告,旧快照里遗留的
|
|
30
|
+
`pending` 也会按快照记录的 `configSource` 归一化显示。
|
|
31
|
+
|
|
32
|
+
### 账本:节省不再是常量 0(净 token 恒负的根因)
|
|
33
|
+
|
|
34
|
+
遥测里「节省 · 剪枝/压缩」长期为 0、净 token 恒为负,是两条**入账腿同时断开**:
|
|
35
|
+
|
|
36
|
+
- **L1 边界剪枝在 0.2.0 web profile 下静默失效** — `dsh-web-app` 的 host 层把
|
|
37
|
+
`tool-result-pruner` 行 `disabled: true` 并移进各 agent preset 的 isolate realm,
|
|
38
|
+
host 平面的 engram 插件 `ctx.get("toolResultPruner")` 拿到 `undefined`,只打一行 warn
|
|
39
|
+
就休眠。现在 bundle `cordis.patch.yml` 重新启用 host 行(预设域仍保留自己那份用于
|
|
40
|
+
compaction 期剪枝),状态快照新增 `boundaryPrune: { armed, reason }`,CLI `status`
|
|
41
|
+
会打印「L1 边界剪枝:已武装 / 未启用(原因)」,未武装时直接提示节省不会入账。
|
|
42
|
+
- **压缩节省从未接入** — Context GC 引擎现在在 checkpoint 提交前算
|
|
43
|
+
`被替换区间可见字符数(含 tool-call 参数与嵌套 tool-result) − checkpoint 字符数`,
|
|
44
|
+
经 `onCompacted` 回调交给账本 `noteCompaction`,按后续请求计入 `avoidedChars`(与剪枝同口径)。
|
|
45
|
+
host 平面直连账本;web 预设域通过 host 插件提供的 `engramLedger` 普通服务桥接(隔离域里
|
|
46
|
+
closure 不可达),全进程仍只有一个落盘写入者。回调失败被容器化,绝不影响压缩。
|
|
47
|
+
|
|
48
|
+
### 遥测口径更直白
|
|
49
|
+
|
|
50
|
+
- 卡片标签改成「节省 · 剪枝/压缩(已入账)」「注入成本 · 索引/召回/ESR(每请求重计)」;
|
|
51
|
+
- 当 `avoidedTokens === 0` 且已有注入/重写时,页签内直接解释:还没有剪枝/压缩节省入账、
|
|
52
|
+
L1 需要 host 平面 pruner、净值为负 = 注入 + 重写罚金;
|
|
53
|
+
- README 中英两版与 `docs/CONTEXT-GC-GUIDE.zh.md` 的「口径诚实」段落更新(原文写
|
|
54
|
+
「Context GC 的压缩节省暂未接入」)。
|
|
55
|
+
|
|
56
|
+
### 兼容性说明
|
|
57
|
+
|
|
58
|
+
- 纯**文件改写逻辑**修复,无存储 schema 变化、无新设置键(GUI 旋钮仍 34 个)。
|
|
59
|
+
- `autoWebCompaction` 仍是 opt-in(默认 `false`):打开后**重启一次 `dsh web`**,下一次启动
|
|
60
|
+
即把每个 stock 声明式预设接管(幂等;已手写的 engram 行会按请求的引擎配置刷新)。
|
|
61
|
+
- 已接管但位于 node_modules 的预设:`npm update dsh` 会恢复出厂布局,保持 autoWebCompaction
|
|
62
|
+
开启时下次启动自动重新接管,否则手动 `npx dsh-engram enable`。
|
|
63
|
+
|
|
64
|
+
### 其他
|
|
65
|
+
|
|
66
|
+
- 测试 **405/405**(v0.7.1 共新增 24 例:15 例 web-provision/CLI——8 个 web-provision 例——声明式 stock/custom/双组拒绝、
|
|
67
|
+
14 空格 swap+refresh+逐字节 restore、`--presets-dir` 发现、roster 无 path 的映射/跳过/扫描
|
|
68
|
+
回退;7 个 CLI 集成例——沙箱 `DSH_HOME` + 声明式目录下 status/enable/revert/`--file`/
|
|
69
|
+
`DSH_WEB_PRESETS_DIR` 发现/doctor opt-in 提示/未知命令退出码;另有 9 例账本修复——
|
|
70
|
+
压缩入账滚动 1、`messagesTextChars` 1、`onCompacted` 上报 1、回调抛错不断压缩 1、
|
|
71
|
+
预设域经 `engramLedger` 入账 1、无账本服务仍可压缩 1、`boundaryPruneAvailability` 3);
|
|
72
|
+
`npm test` 全绿。
|
|
73
|
+
- 文档:`docs/CONTEXT-GC-GUIDE.zh.md` 补两种预设布局说明、`.engram.bak` 命名、`--presets-dir`、
|
|
74
|
+
两条排障条目;顺带修正指南第 6 节里 v0.6 起已改为 opt-in 但表格仍写默认 `true` 的笔误;
|
|
75
|
+
README 兼容性矩阵新增 `0.7.1` 行。
|
|
76
|
+
|
|
77
|
+
## DSH 版本支持
|
|
78
|
+
|
|
79
|
+
- **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
|
|
80
|
+
- **peer 声明**:
|
|
81
|
+
- `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
82
|
+
- `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
83
|
+
- `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
84
|
+
- `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
85
|
+
- `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
86
|
+
- **验证版本**:`dsh 0.2.0-rc.2`(本机全局安装,`dsh-web-app/presets` 4 个预设实测)
|
|
87
|
+
- **最后验证日期**:2026-10-19
|
|
88
|
+
- **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
|
package/lib/anchor.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-engram revalidatable file anchors (PROPOSAL-evolution §4 v0.7-C).
|
|
3
|
+
*
|
|
4
|
+
* "The past is treated as truth" is the strongest critique against memory
|
|
5
|
+
* plugins: a fact written last week may describe code that changed yesterday.
|
|
6
|
+
* An anchor makes staleness DETECTABLE instead of trusted:
|
|
7
|
+
*
|
|
8
|
+
* { path, mtimeMs, size, hash?, at }
|
|
9
|
+
*
|
|
10
|
+
* - `makeFileAnchor` records the file's identity when a memory is written
|
|
11
|
+
* (sha256 for small files, mtime+size for every file — cheap default);
|
|
12
|
+
* - `anchorStatus` re-stats (and re-hashes when a hash exists) and returns
|
|
13
|
+
* `valid | changed | missing`;
|
|
14
|
+
* - `collectStaleAnchors` scans a bounded set of active memories so the index
|
|
15
|
+
* / entity truth can drop rows whose source moved, and the caller can tag
|
|
16
|
+
* them `stale` (never deleted — still re-fetchable via engram_detail).
|
|
17
|
+
*
|
|
18
|
+
* Zero LLM, fail-contained: any filesystem error reads as "missing", never
|
|
19
|
+
* throws into a render path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { readFileSync, statSync } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MAX_HASH_BYTES = 256 * 1024;
|
|
27
|
+
|
|
28
|
+
function hashFile(file, size) {
|
|
29
|
+
try {
|
|
30
|
+
return createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 32);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function absolute(filePath, baseDir) {
|
|
37
|
+
const raw = String(filePath ?? "").trim();
|
|
38
|
+
if (raw.length === 0) return "";
|
|
39
|
+
if (path.isAbsolute(raw)) return raw;
|
|
40
|
+
return typeof baseDir === "string" && baseDir.length > 0 ? path.resolve(baseDir, raw) : path.resolve(raw);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build an anchor for an existing file, or null when it cannot be stat'ed.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} filePath file path as stored on the memory
|
|
47
|
+
* @param {object} [opts]
|
|
48
|
+
* @param {string} [opts.baseDir] directory for relative paths (the workspace)
|
|
49
|
+
* @param {number} [opts.now]
|
|
50
|
+
* @param {number} [opts.maxHashBytes] hash only files up to this size
|
|
51
|
+
*/
|
|
52
|
+
export function makeFileAnchor(filePath, { baseDir = "", now = Date.now(), maxHashBytes = DEFAULT_MAX_HASH_BYTES } = {}) {
|
|
53
|
+
const abs = absolute(filePath, baseDir);
|
|
54
|
+
if (abs.length === 0) return null;
|
|
55
|
+
try {
|
|
56
|
+
const stat = statSync(abs);
|
|
57
|
+
if (!stat.isFile()) return null;
|
|
58
|
+
return {
|
|
59
|
+
path: abs,
|
|
60
|
+
mtimeMs: stat.mtimeMs,
|
|
61
|
+
size: stat.size,
|
|
62
|
+
hash: stat.size <= maxHashBytes ? hashFile(abs, stat.size) : null,
|
|
63
|
+
at: now,
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Revalidate one anchor: `valid` | `changed` | `missing` (`none` when absent).
|
|
72
|
+
*/
|
|
73
|
+
export function anchorStatus(anchor) {
|
|
74
|
+
if (anchor === null || anchor === void 0 || typeof anchor.path !== "string" || anchor.path.length === 0) {
|
|
75
|
+
return "none";
|
|
76
|
+
}
|
|
77
|
+
let stat;
|
|
78
|
+
try {
|
|
79
|
+
stat = statSync(anchor.path);
|
|
80
|
+
} catch {
|
|
81
|
+
return "missing";
|
|
82
|
+
}
|
|
83
|
+
if (!stat.isFile()) return "missing";
|
|
84
|
+
if (stat.size !== anchor.size || stat.mtimeMs !== anchor.mtimeMs) {
|
|
85
|
+
// mtime can survive content changes only in exotic cases; when a hash was
|
|
86
|
+
// recorded, let it decide before declaring the file changed.
|
|
87
|
+
if (typeof anchor.hash !== "string") return "changed";
|
|
88
|
+
const hash = hashFile(anchor.path, stat.size);
|
|
89
|
+
return hash === anchor.hash ? "valid" : "changed";
|
|
90
|
+
}
|
|
91
|
+
if (typeof anchor.hash === "string") {
|
|
92
|
+
const hash = hashFile(anchor.path, stat.size);
|
|
93
|
+
return hash === anchor.hash ? "valid" : "changed";
|
|
94
|
+
}
|
|
95
|
+
return "valid";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Is this memory's source still the same file it was written against? */
|
|
99
|
+
export function memoryIsStale(memory) {
|
|
100
|
+
if (memory === null || typeof memory !== "object") return false;
|
|
101
|
+
const status = anchorStatus(memory.anchor);
|
|
102
|
+
return status === "changed" || status === "missing";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Bounded scan of active anchored memories whose source moved/vanished.
|
|
107
|
+
* Returns [{ id, status, path }] and never throws.
|
|
108
|
+
*/
|
|
109
|
+
export function collectStaleAnchors(store, workspace, { limit = 40 } = {}) {
|
|
110
|
+
if (store === null || typeof store.listMemories !== "function") return [];
|
|
111
|
+
const cap = Number.isFinite(limit) && limit > 0 ? Math.trunc(limit) : 40;
|
|
112
|
+
const rows = store.listMemories(workspace, 200);
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const memory of rows) {
|
|
115
|
+
if (out.length >= cap) break;
|
|
116
|
+
if (memory?.anchor === null || memory?.anchor === void 0) continue;
|
|
117
|
+
if (Array.isArray(memory.tags) && memory.tags.includes("stale")) continue;
|
|
118
|
+
const status = anchorStatus(memory.anchor);
|
|
119
|
+
if (status === "changed" || status === "missing") {
|
|
120
|
+
out.push({ id: memory.id, status, path: memory.anchor.path });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
package/lib/boundary-prune.js
CHANGED
|
@@ -45,6 +45,7 @@ import { workspaceKey as wk } from "./util.js";
|
|
|
45
45
|
|
|
46
46
|
/** Fixed fallback for the repayment gate when no history exists yet. */
|
|
47
47
|
const DEFAULT_MIN_REMAINING = 50;
|
|
48
|
+
|
|
48
49
|
/**
|
|
49
50
|
* Empirical expected session length (requests) for coding workspaces, from
|
|
50
51
|
* the L0 corpus (97 sessions, median 43, long tail 1247). Used as the prior
|
|
@@ -128,6 +129,32 @@ export function expectedSessionRequests(requestsSoFar, history, workspace, floor
|
|
|
128
129
|
return Math.max(prior, requestsSoFar * 2);
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Whether BoundaryPrune can actually fire on this context, for the durable
|
|
134
|
+
* status snapshot (and for users asking "why is 节省 always 0?").
|
|
135
|
+
*
|
|
136
|
+
* DSH ≥ 0.2.0 web profiles disable the host-plane `tool-result-pruner` row and
|
|
137
|
+
* mount the pruner inside each agent preset's isolate realm, where a host-plane
|
|
138
|
+
* plugin cannot see it. The shipped bundle patch re-enables the host row; when
|
|
139
|
+
* a custom profile keeps it off, this reports why L1 stays dormant.
|
|
140
|
+
*
|
|
141
|
+
* @param {object} ctx - the host plugin context.
|
|
142
|
+
* @param {object} config - resolved engram config.
|
|
143
|
+
* @returns {{ armed: boolean, reason: string }}
|
|
144
|
+
*/
|
|
145
|
+
export function boundaryPruneAvailability(ctx, config) {
|
|
146
|
+
if (config?.boundaryPrune === false) return { armed: false, reason: "disabled by config (boundaryPrune:false)" };
|
|
147
|
+
const pruner = ctx?.get?.("toolResultPruner");
|
|
148
|
+
if (!pruner || typeof pruner.pruneSession !== "function") {
|
|
149
|
+
return {
|
|
150
|
+
armed: false,
|
|
151
|
+
reason: "no toolResultPruner service in the host plane (the bundle patch must re-enable the tool-result-pruner row)",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const threshold = pruner.config?.thresholdChars;
|
|
155
|
+
return { armed: true, reason: `threshold ${Number.isFinite(threshold) ? threshold : "default"}${config?.boundaryPruneThresholdChars > 0 ? ` (override ${config.boundaryPruneThresholdChars})` : ""}` };
|
|
156
|
+
}
|
|
157
|
+
|
|
131
158
|
/** Install the boundary-prune listener. Returns a disposer. */
|
|
132
159
|
export function installBoundaryPrune(ctx, config, log, hooks = {}, opts = {}) {
|
|
133
160
|
// Default ON (v0.6 default correction): explicit `false` is the opt-out.
|
package/lib/client.js
CHANGED
|
@@ -2209,11 +2209,11 @@ function EngramTelemetry({ api, workspace }) {
|
|
|
2209
2209
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.grid, marginTop: 8 }, children: [
|
|
2210
2210
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
|
|
2211
2211
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#059669" }, children: tokenCount(ledgerBucket.avoidedTokens) }),
|
|
2212
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u8282\u7701 \xB7 \u526A\u679D/\u538B\u7F29" })
|
|
2212
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u8282\u7701 \xB7 \u526A\u679D/\u538B\u7F29\uFF08\u5DF2\u5165\u8D26\uFF09" })
|
|
2213
2213
|
] }),
|
|
2214
2214
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
|
|
2215
2215
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#d97706" }, children: tokenCount(-ledgerBucket.injectedTokens) }),
|
|
2216
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u6CE8\u5165\u6210\u672C \xB7 \u7D22\u5F15/\u53EC\u56DE/ESR" })
|
|
2216
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u6CE8\u5165\u6210\u672C \xB7 \u7D22\u5F15/\u53EC\u56DE/ESR\uFF08\u6BCF\u8BF7\u6C42\u91CD\u8BA1\uFF09" })
|
|
2217
2217
|
] }),
|
|
2218
2218
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
|
|
2219
2219
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#dc2626" }, children: tokenCount(-Math.round(ledgerBucket.rewritesCostChars / ledger.charsPerToken)) }),
|
|
@@ -2224,6 +2224,7 @@ function EngramTelemetry({ api, workspace }) {
|
|
|
2224
2224
|
] })
|
|
2225
2225
|
] })
|
|
2226
2226
|
] }),
|
|
2227
|
+
ledgerBucket.avoidedTokens === 0 && (ledgerBucket.injectedTokens > 0 || ledgerBucket.rewriteCount > 0) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.sub, marginTop: 6 }, children: "\u8282\u7701\u4ECD\u4E3A 0\uFF1A\u672C\u5DE5\u4F5C\u533A\u8FD8\u6CA1\u6709\u526A\u679D/\u538B\u7F29\u8282\u7701\u5165\u8D26\u2014\u2014L1 \u8FB9\u754C\u526A\u679D\u9700\u8981 host \u5E73\u9762\u7684 toolResultPruner\uFF08DSH \u2265 0.2.0 \u7531 bundle patch \u91CD\u65B0\u542F\u7528\u8BE5\u884C\uFF09\uFF0C\u538B\u7F29\u8282\u7701\u7531 Context GC \u5F15\u64CE\u5728 checkpoint \u65F6\u4E0A\u62A5\uFF1B\u51C0\u503C\u4E3A\u8D1F = \u6CE8\u5165\u6210\u672C + \u91CD\u5199\u7F5A\u91D1\u7684\u5408\u8BA1\u3002" }),
|
|
2227
2228
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.sub, marginTop: 6 }, children: [
|
|
2228
2229
|
"\u53E3\u5F84\uFF1Achars/",
|
|
2229
2230
|
ledger.charsPerToken,
|
|
@@ -2714,6 +2715,7 @@ function signedTokens(n) {
|
|
|
2714
2715
|
}
|
|
2715
2716
|
var KIND_COLORS2 = {
|
|
2716
2717
|
preference: "#b45309",
|
|
2718
|
+
episode: "#0e7490",
|
|
2717
2719
|
decision: "#2563eb",
|
|
2718
2720
|
error: "#dc2626",
|
|
2719
2721
|
procedure: "#7c3aed",
|
|
@@ -3703,6 +3705,9 @@ var COMMON_FIELDS = [
|
|
|
3703
3705
|
{ key: "resultPack", label: "\u7ED3\u679C\u5F52\u6863\u53EC\u56DE", hint: "ResultPack L2\uFF1A\u526A\u6389\u7684\u4E2D\u6BB5\u8131\u654F\u5F52\u6863\uFF0C\u8282\u70B9\u5C3E\u90E8\u9644 handle\uFF0C\u6A21\u578B\u53EF\u7528 result_recall(callId) \u6309\u9875\u53EC\u56DE\uFF08\u5B8C\u6574 ObservationPack\uFF1B\u9ED8\u8BA4\u5F00\uFF0C\u4FDD\u8BC1\u9ED8\u8BA4\u526A\u679D\u65E0\u635F\uFF1B\u9700\u4E0E\u8FB9\u754C\u526A\u679D\u4E00\u8D77\u5F00\uFF09", kind: "bool" },
|
|
3704
3706
|
{ key: "autoRecallOnStart", label: "\u4F1A\u8BDD\u542F\u52A8\u81EA\u52A8\u53EC\u56DE", hint: "\u65B0\u4F1A\u8BDD\u6309\u9996\u6761\u6D88\u606F\u81EA\u52A8\u628A\u6700\u76F8\u5173\u8BB0\u5FC6\u6CE8\u5165 [RECALL] \u5757\uFF08\u65E0\u9700\u6A21\u578B\u60F3\u8D77\u8C03 engram_recall\uFF09\uFF1B\u5173\u6389=\u56DE\u5230\u7EAF\u62C9\u53D6", kind: "bool" },
|
|
3705
3707
|
{ key: "jitEnabled", label: "JIT \u63A8\u9001", hint: "\u9519\u8BEF\u590D\u53D1 / \u6587\u4EF6\u7126\u70B9 / goal \u5B8C\u6210\u65F6\uFF0C\u628A\u5DF2\u6709\u8BB0\u5FC6\u538B\u6210\u4E00\u884C\u63A8\u8FDB\u4E0A\u4E0B\u6587\uFF08append-only\u3001\u6BCF\u4F1A\u8BDD\u22643 \u884C\u3001\u65E0\u547D\u4E2D\u6C89\u9ED8\uFF1B\u6BCF\u6B21\u63A8\u9001\u6309\u524D\u7F00\u91CD\u5199\u8BA1\u5165\u8D26\u672C\uFF09", kind: "bool" },
|
|
3708
|
+
{ key: "episodeEnabled", label: "\u4F1A\u8BDD\u7EAA\u8981", hint: "\u4F1A\u8BDD\u7ED3\u675F\u65F6\u7528\u96F6 LLM \u89C4\u5219\u751F\u6210\u4E00\u6761 episode\uFF08\u6587\u4EF6/\u547D\u4EE4/\u5931\u8D25\u2192\u4FEE\u590D/\u51B3\u7B56/\u672A\u95ED\u73AF\uFF09\uFF0C\u4E0D\u8FDB\u7D22\u5F15\uFF0C\u4F9B\u6062\u590D\u7B80\u62A5\u4F7F\u7528\uFF1B\u6BCF workspace \u4FDD\u7559\u6700\u8FD1 20 \u6761", kind: "bool" },
|
|
3709
|
+
{ key: "resumeBriefEnabled", label: "\u6062\u590D\u7B80\u62A5", hint: "\u65B0\u4F1A\u8BDD\u542F\u52A8\u65F6\u6CE8\u5165\u6700\u8FD1 7 \u5929\u7684\u6700\u65B0 episode\uFF08\u2264240 \u5B57\u7B26 + engram_detail \u6307\u9488\uFF09\uFF0C\u9694\u5929\u7EE7\u7EED\u4E0D\u7528\u91CD\u65B0\u4EA4\u4EE3\u80CC\u666F", kind: "bool" },
|
|
3710
|
+
{ key: "entityCardsEnabled", label: "\u5B9E\u4F53\u5361", hint: "[ENGRAM] \u7D22\u5F15\u4E3A\u6D3B\u8DC3\u5B9E\u4F53\uFF08\u6709\u951A\u5B9A\u8BB0\u5FC6\u6216\u672A\u95ED\u73AF\u4EFB\u52A1\uFF09\u6E32\u67D3\u4E00\u884C\u300C\u5F53\u524D\u771F\u76F8 + \u6253\u5F00\u4EFB\u52A1 + engram_detail(entity=\u2026) \u6307\u9488\u300D\uFF0C\u6700\u591A 2 \u5F20\uFF0C\u66FF\u4EE3\u8FD9\u4E9B\u5B9E\u4F53\u7684\u5E73\u94FA\u8BB0\u5FC6\u884C", kind: "bool" },
|
|
3706
3711
|
{ key: "maxRecallPerSession", label: "\u6BCF\u4F1A\u8BDD\u53BB\u91CD\u4E0A\u9650", hint: "recall \u6BCF\u4F1A\u8BDD\u6700\u591A\u4FDD\u7559 N \u6761\uFF08\u8DE8\u4F1A\u8BDD\u53BB\u91CD\uFF09", kind: "num", min: 1, max: 10 },
|
|
3707
3712
|
{ key: "expireDays", label: "TTL\uFF08\u5929\uFF09", hint: "0 = \u4E0D\u8FC7\u671F", kind: "num", min: 0, max: 3650 },
|
|
3708
3713
|
{ key: "indexMaxChars", label: "\u7D22\u5F15\u5B57\u7B26\u4E0A\u9650", hint: "[ENGRAM] \u5757 token \u9884\u7B97", kind: "num", min: 0, max: 4e3 }
|
package/lib/compaction.js
CHANGED
|
@@ -62,6 +62,17 @@ export function apply(ctx, config = {}) {
|
|
|
62
62
|
if (!domain) return null;
|
|
63
63
|
return collectPointerContext(domain, wk(workspace), report);
|
|
64
64
|
},
|
|
65
|
+
// Context Ledger bridge: this row runs inside the agent preset's
|
|
66
|
+
// isolate realm, where the host plugin's ledger closure is not
|
|
67
|
+
// reachable. The host plugin provides `engramLedger` as a plain
|
|
68
|
+
// (non-isolated) service, so compaction savings are credited to the
|
|
69
|
+
// one durable ledger instead of vanishing with the realm.
|
|
70
|
+
onCompacted: ({ session, workspace, charsSaved }) => {
|
|
71
|
+
try {
|
|
72
|
+
const hub = ctx.get("engramLedger");
|
|
73
|
+
hub?.noteCompaction?.(session?.id, workspace, { charsSaved });
|
|
74
|
+
} catch { /* contained: accounting never breaks compaction */ }
|
|
75
|
+
},
|
|
65
76
|
}
|
|
66
77
|
: {};
|
|
67
78
|
|
package/lib/context-gc.js
CHANGED
|
@@ -73,6 +73,38 @@ export function blockText(content) {
|
|
|
73
73
|
return out.trim();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Recursive visible-text char count over DSH content blocks (code points). */
|
|
77
|
+
function contentTextChars(value) {
|
|
78
|
+
if (typeof value === "string") return [...value].length;
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
let chars = 0;
|
|
81
|
+
for (const part of value) chars += contentTextChars(part);
|
|
82
|
+
return chars;
|
|
83
|
+
}
|
|
84
|
+
if (value === null || typeof value !== "object") return 0;
|
|
85
|
+
let chars = 0;
|
|
86
|
+
// Text blocks, tool-call arguments and nested tool-result content are all
|
|
87
|
+
// replayed context — count each once.
|
|
88
|
+
if (typeof value.text === "string") chars += [...value.text].length;
|
|
89
|
+
if (typeof value.arguments === "string") chars += [...value.arguments].length;
|
|
90
|
+
if (value.content !== undefined) chars += contentTextChars(value.content);
|
|
91
|
+
return chars;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Total visible text characters (Unicode code points) across a message list —
|
|
96
|
+
* the size side of the Context Ledger credit: a compaction replaces this span
|
|
97
|
+
* with the checkpoint, so `span − summary` is the part that stops replaying.
|
|
98
|
+
* @param {Array<object>} messages
|
|
99
|
+
* @returns {number}
|
|
100
|
+
*/
|
|
101
|
+
export function messagesTextChars(messages) {
|
|
102
|
+
if (!Array.isArray(messages)) return 0;
|
|
103
|
+
let chars = 0;
|
|
104
|
+
for (const message of messages) chars += contentTextChars(message?.content);
|
|
105
|
+
return chars;
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
/**
|
|
77
109
|
* The durable checkpoint framing the harness applies to a compaction summary
|
|
78
110
|
* before accepting it (`frameSummary()` in dsh-compaction-basic): the summary
|
|
@@ -762,6 +794,21 @@ export async function loadContextGcEngine(ctx, config = {}, deps = {}) {
|
|
|
762
794
|
}
|
|
763
795
|
summaryText = fit.text;
|
|
764
796
|
|
|
797
|
+
// Context Ledger credit: the span the checkpoint replaces stops
|
|
798
|
+
// replaying, so `span − summary` is a measured saving the accounting
|
|
799
|
+
// layer never had (the host does not expose compaction sizes). Best
|
|
800
|
+
// effort — accounting must never break the compaction path.
|
|
801
|
+
try {
|
|
802
|
+
if (typeof this.gcDeps?.onCompacted === "function") {
|
|
803
|
+
const spanChars = messagesTextChars(messages);
|
|
804
|
+
const summaryChars = [...summaryText].length;
|
|
805
|
+
const charsSaved = spanChars - summaryChars;
|
|
806
|
+
if (charsSaved > 0) {
|
|
807
|
+
this.gcDeps.onCompacted({ session: agent?.session, workspace: ws, charsSaved, spanChars, summaryChars });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch { /* contained: a credit bug must never fail a compaction */ }
|
|
811
|
+
|
|
765
812
|
const result = { summary: [{ type: "text", text: summaryText }] };
|
|
766
813
|
if (narrative.llm && typeof narrative.llm === "object") Object.assign(result, narrative.llm);
|
|
767
814
|
// The harness's durable `compaction/summary` record ALWAYS carries
|