dsh-engram 0.7.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 CHANGED
@@ -264,8 +264,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
264
264
  <!-- dsh-market compatibility block — keep labels bilingual and in sync with
265
265
  docs/RELEASE-NOTES.TEMPLATE.md (market README convention) -->
266
266
 
267
- - **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.7.0`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
268
- - **最后验证日期 / Last verified**: 2026-10-19 — episode / entity pages / anchor revalidation,npm test 381/381 通过
267
+ - **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.7.1`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
268
+ - **最后验证日期 / Last verified**: 2026-10-19 — web-plane Context GC 适配 DSH `0.2.0` 声明式预设(`dsh-web-app/presets/*.patch.yml`),npm test 396/396 通过
269
269
  - **安装方式已验证 / Verified install**: `dsh plugin --profile web add dsh-engram`
270
270
 
271
271
  > **Short version.** Every **published** release (`0.2.0` … `0.3.6`) supports
@@ -282,7 +282,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
282
282
  | `0.5.0` (npm) | published | **DSH `0.1.2-alpha.2` … `<0.3.0-0`** — 设置卡迁移到 entry-id volatile Config(`0.1.7+`/`0.2.x`),`≤0.1.5` 自动回退 `installSection` | `engines.dsh` + peers `>=0.1.2-alpha.2 <0.3.0-0` |
283
283
  | `0.5.1` | published | 同 `0.5.0` — 会话扫描修复(`session.v<N>.jsonl.zstd`,issue #8)、数据导出/导入 GUI、替换/冲突可视化(issue #3) | 同 `0.5.0` |
284
284
  | `0.6.0` | published | 同 `0.5.0` — Context Ledger、BoundaryPrune/ResultPack 默认开 + 账本否决、JIT 推送、Context GC 改 opt-in | 同 `0.5.0` |
285
- | `0.7.0` (本期 / this release) | published | 同 `0.5.0` — session episode、resume brief、entity pages/cards、revalidatable anchors + layered TTL | 同 `0.5.0` |
285
+ | `0.7.0` | published | 同 `0.5.0` — session episode、resume brief、entity pages/cards、revalidatable anchors + layered TTL | 同 `0.5.0` |
286
+ | `0.7.1` (本期 / this release) | prepared | 同 `0.5.0` — web 平面 Context GC 适配 DSH `0.2.0` 声明式预设(`dsh-web-app/presets/*.patch.yml`,14 空格缩进),状态/doctor CLI 同步 | 同 `0.5.0` |
286
287
 
287
288
  ### Where the API family broke — `0.1.2-alpha.2`
288
289
 
@@ -332,7 +333,37 @@ drives mutations).
332
333
  So: keep DSH on `0.1.1-rc.2` or older while running a release ≤ `0.3.6`, and
333
334
  upgrade the plugin to `>=0.3.7` before moving DSH to `0.1.2-alpha.2`.
334
335
 
335
- ### Current release — `0.7.0`
336
+ ### Current release — `0.7.1`
337
+
338
+ Web-plane Context GC on DSH `0.2.0`'s **declarative presets** — the preset
339
+ format changed shape (one `@deepseek-ai/dsh-agent-preset` declaration inside
340
+ `dsh-web-app/presets/*.patch.yml`), and the auto-wiring was anchored to the old
341
+ `agent.cordis.yml` layout, so `autoWebCompaction` silently skipped every preset
342
+ (and could half-rewrite a stock row: the pattern did not include its trailing
343
+ newline).
344
+
345
+ - **Structural row location** — the `compaction` group is found by YAML
346
+ structure (a list item carrying `group: true`), not by column position; the
347
+ stock/engram rows are validated by BOTH their `- id:` and `name:` values, and
348
+ the replacement inherits the row's own indent. Legacy `agent.cordis.yml`
349
+ (4 spaces) and declarative patch files (14/16/18 spaces) both round-trip
350
+ byte-identically.
351
+ - **Declarative discovery** — the preset roster no longer carries `path` on
352
+ DSH `0.2.0`; the plugin maps each roster id to the shipped
353
+ `<presetsDir>/<id>.patch.yml` (resolved through the `dsh` install, with
354
+ `DSH_WEB_PRESETS_DIR` override) and skips user-defined declarative presets
355
+ (profile-patch overrides) with an explicit log line instead of failing.
356
+ - **Status/doctor** — `npx dsh-engram status`/`doctor` discover both preset
357
+ generations, show the detected layout, and flag `autoWebCompaction=false`
358
+ (the opt-in toggle that makes the web plane a no-op), node_modules preset
359
+ wiring (survives `npm update dsh` only while auto-wiring stays on), and
360
+ profile-patch `preset-*` overrides that outrank the shipped files.
361
+ - **Migration** — restart `dsh web` with `autoWebCompaction:true`: the next boot
362
+ re-wires every stock declarative preset (idempotent refresh; existing
363
+ hand-wired rows are rewritten to the requested engine config). `npm test`
364
+ 396/396.
365
+
366
+ ### Previous release — `0.7.0`
336
367
 
337
368
  From event records to **maintainable current truth**: sessions sediment an
338
369
  episode at teardown, new sessions get a resume brief, `esr_node` becomes a
@@ -378,7 +409,7 @@ namespace via `settings.installSection`; on `≥0.1.7` / `0.2.x` the form is the
378
409
  plugin's `Config` with the 34 GUI knobs marked `.volatile()` (entry id
379
410
  `engram`). The browser settings card reads and writes through the plugin's own
380
411
  HTTP route (`/api/dsh-engram/settings`, see Route B above) on both. The full
381
- regression suite (npm test 329/329, incl. the new settings-v2 coverage for the
412
+ regression suite (npm test 396/396, incl. the new settings-v2 coverage for the
382
413
  volatile-Config model) passes against the DSH `0.2.0-rc.2` dependency set
383
414
  (cordis 4.0.4 + schemastery 3.18.4 + cosmokit, matching the global DSH CLI).
384
415
  The `.volatile()`/loader hot-commit path and the describe-side projection were
@@ -678,10 +709,15 @@ default):
678
709
  builds its `Include.Config` with no patches; verified against the harness
679
710
  source). So on boot, once the `agentPresets` service is ready, the plugin
680
711
  **auto-edits every preset that still has the stock layout** — the default
681
- preset plus the whole roster (shipped root + `~/.dsh/.agent-presets` user
682
- root: `standard`, `code`, `cordis`, and your own presets): it swaps the
683
- `compaction-basic` row inside the `compaction` group for
684
- `dsh-engram/compaction`. That auto-wiring is:
712
+ preset plus the whole roster, across BOTH preset generations: legacy
713
+ `<root>/<id>/agent.cordis.yml` compositions (DSH ≤ 0.1.x, row at 4 spaces)
714
+ and the declarative `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml` files
715
+ (DSH ≥ 0.2.0, row at 14 spaces inside the `@deepseek-ai/dsh-agent-preset`
716
+ declaration; a user-defined declarative preset with no shipped file is left
717
+ alone). The row is located by YAML structure (a list item carrying
718
+ `group: true`) and validated by both its `- id:` and `name:` values, so the
719
+ swap is column-position independent: it swaps the `compaction-basic` row
720
+ inside the `compaction` group for `dsh-engram/compaction`. That auto-wiring is:
685
721
  - **off by default (opt-in)** (`autoWebCompaction: false`, toggle in the
686
722
  settings card's memory-GC section); once enabled it is idempotent — an
687
723
  already-wired preset is left alone;
@@ -691,8 +727,9 @@ default):
691
727
  - **per-preset**: only stock-layout presets are touched — a preset with a
692
728
  custom compaction group, or none at all (e.g. the shipped `minimal`
693
729
  preset), is never written, only logged;
694
- - **backed up + validated**: a create-only `agent.cordis.yml.engram.bak`
695
- holds each original next to its file, every result is re-read and verified
730
+ - **backed up + validated**: a create-only `.engram.bak`
731
+ (`agent.cordis.yml.engram.bak` or `<id>.patch.yml.engram.bak`) holds each
732
+ original next to its file, every result is re-read and verified
696
733
  after writing, and any doubt rolls that file back; a failure only warns and
697
734
  that session keeps DSH's default summarizer — the host plane is never
698
735
  affected.
@@ -702,7 +739,9 @@ default):
702
739
  changing the settings knobs re-provisions the web rows on the next boot
703
740
  instead of being silently ignored.
704
741
  - A fully equivalent manual CLI, shipped in the npm package
705
- (scans both the shipped and the user root; `--file <path>` targets one):
742
+ (scans both preset generations — legacy roots, the shipped declarative
743
+ presets, and the paths recorded at boot; `--presets-dir <dir>` overrides
744
+ declarative discovery, `--file <path>` targets one):
706
745
  ```bash
707
746
  npx dsh-engram status # stock = untouched · wired = active · custom = hands-off
708
747
  npx dsh-engram doctor # status + ranked next steps for every gap found
@@ -872,10 +911,25 @@ adds a "net context tokens" stat card. Durable rollup at
872
911
  `~/.dsh/engram/ledger.json` (atomic write, daily buckets, delta flush); the hot
873
912
  path is an integer add, zero I/O and zero model calls.
874
913
  Defaults: `ledgerEnabled: true`, `ledgerCacheDiscount: 0.1`; off = no listeners,
875
- no writes. **Honest scope**: tokens are `chars / 4` estimates, not provider
876
- billing, and no quality delta is claimed; Context GC compaction savings are not
877
- wired yet (the host does not expose before/after sizes), so the ledger only
878
- counts what it can prove.
914
+ no writes.
915
+
916
+ **Two credit legs** (v0.7.1 — before this the ledger only charged costs, so the
917
+ net was structurally negative):
918
+
919
+ 1. **L1 boundary prune** — chars removed by `toolResultPruner`, credited per
920
+ later request. Requires a host-plane `toolResultPruner`; DSH ≥ 0.2.0 web
921
+ profiles move that row into each agent preset's isolate realm, so the bundle
922
+ patch re-enables the host row — check `boundaryPrune.armed` in the status
923
+ snapshot.
924
+ 2. **Compaction (Context GC)** — every checkpoint credits `span chars −
925
+ checkpoint chars` from the engine that produced it; host plane and the web
926
+ preset realm (bridged through the `engramLedger` service) write the same
927
+ ledger.
928
+
929
+ **Honest scope**: tokens are `chars / 4` estimates, not provider billing, and no
930
+ quality delta is claimed. Until a workspace actually prunes or compacts its
931
+ "saved" leg is 0, so the net equals injected + rewrite penalty (negative is
932
+ expected) — the Telemetry tab says so inline.
879
933
 
880
934
  ### JIT push (v0.6-C)
881
935
 
package/README.zh.md CHANGED
@@ -166,8 +166,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
166
166
 
167
167
  <!-- dsh-market 兼容信息块 —— 与 docs/RELEASE-NOTES.TEMPLATE.md 的市场 README 公约保持一致 -->
168
168
 
169
- - **DSH 版本**:`>=0.1.2-alpha.2 <0.3.0-0`(本版 `0.7.0`;已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` —— 见下文矩阵)
170
- - **最后验证日期**:2026-10-19 —— episode / 实体页 / 锚点重验证,npm test 381/381 通过
169
+ - **DSH 版本**:`>=0.1.2-alpha.2 <0.3.0-0`(本版 `0.7.1`;已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` —— 见下文矩阵)
170
+ - **最后验证日期**:2026-10-19 —— web 平面 Context GC 适配 DSH `0.2.0` 声明式预设(`dsh-web-app/presets/*.patch.yml`),npm test 396/396 通过
171
171
  - **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
172
172
 
173
173
  > **一句话结论**:所有**已真正发布**的版本(`0.2.0` … `0.3.6`)**只支持旧版
@@ -182,7 +182,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
182
182
  | `0.5.0`(npm) | 已发布 | **DSH `0.1.2-alpha.2` … `<0.3.0-0`** —— 设置卡迁移到 entry-id volatile Config(`0.1.7+`/`0.2.x`),`≤0.1.5` 自动回退 `installSection` | `engines.dsh` + peers `>=0.1.2-alpha.2 <0.3.0-0` |
183
183
  | `0.5.1` | 已发布 | 同 `0.5.0` —— 会话扫描修复(`session.v<N>.jsonl.zstd`,issue #8)、数据导出/导入 GUI、替换/冲突可视化(issue #3) | 同 `0.5.0` |
184
184
  | `0.6.0` | 已发布 | 同 `0.5.0` —— Context Ledger、BoundaryPrune/ResultPack 默认开 + 账本否决、JIT 推送、Context GC 改 opt-in | 同 `0.5.0` |
185
- | `0.7.0`(本期 / this release) | 已发布 | 同 `0.5.0` —— 会话 episode、恢复简报、实体页/实体卡、可重验证锚点 + 分层 TTL | 同 `0.5.0` |
185
+ | `0.7.0` | 已发布 | 同 `0.5.0` —— 会话 episode、恢复简报、实体页/实体卡、可重验证锚点 + 分层 TTL | 同 `0.5.0` |
186
+ | `0.7.1`(本期 / this release) | 待发布 | 同 `0.5.0` —— web 平面 Context GC 适配 DSH `0.2.0` 声明式预设(`dsh-web-app/presets/*.patch.yml`,14 空格缩进),status/doctor CLI 同步 | 同 `0.5.0` |
186
187
 
187
188
  ### API 族断裂点:`0.1.2-alpha.2`
188
189
 
@@ -222,7 +223,25 @@ profile 条目 id(`engram`)为命名空间,不再需要任何“挂载”
222
223
  所以:跑 ≤ `0.3.6` 的已发布版本时请把 DSH 保持在 `0.1.1-rc.2` 或更旧;升级
223
224
  DSH 到 `0.1.2-alpha.2` 之前,先升级插件到 `>=0.3.7`。
224
225
 
225
- ### 当前发布版本 —— `0.7.0`
226
+ ### 当前发布版本 —— `0.7.1`
227
+
228
+ 修复 **DSH `0.2.0` 声明式预设下的 web 平面 Context GC 接管**:旧版自动装配以
229
+ `agent.cordis.yml` 的 4 空格布局为锚,而 `0.2.0` 把预设换成
230
+ `dsh-web-app/presets/<id>.patch.yml` 里的 `@deepseek-ai/dsh-agent-preset` 声明(行在
231
+ 14/16/18 空格缩进),导致 `autoWebCompaction` 静默跳过所有预设。
232
+
233
+ - **结构化定位 compaction 行**:找 `group: true` 的 `compaction` 组,组内按 `- id:` +
234
+ `name:` 双重校验定位 stock/engram 行,替换块继承该行自己的缩进;旧版与声明式布局都
235
+ swap → restore 逐字节还原。顺带修掉旧 stock 替换串不含行尾换行、可能半改写文件的隐患。
236
+ - **声明式预设发现**:roster 不再带 `path` 时把 id 映射到出厂 `<presetsDir>/<id>.patch.yml`
237
+ (`DSH_WEB_PRESETS_DIR` 可覆盖);无出厂文件的用户自定义声明式预设跳过并打日志。
238
+ - **status/doctor 适配**:自动发现两种布局并显示布局类型;doctor 新增
239
+ `autoWebCompaction=false`、node_modules 预设(`npm update dsh` 会恢复出厂布局)、
240
+ profile patch `preset-*` 覆写(优先于 shipped 文件)三类提示。
241
+ - **迁移**:打开 `autoWebCompaction` 后重启一次 `dsh web`,下一次启动即幂等接管所有
242
+ stock 声明式预设;`npm test` 396/396。
243
+
244
+ ### 上一发布版本 —— `0.7.0`
226
245
 
227
246
  把「记忆」从**事件记录**推进到**可维护的当前真相**:会话结束自动沉淀 episode、
228
247
  新会话给恢复简报、`esr_node` 升级为实体页、文件锚点让过期真相可被检测。
@@ -256,7 +275,7 @@ Config** 模型(并带特性检测的旧代际回退)。
256
275
  走 `settings.installSection` 注册 `dsh-engram` 命名空间;`≥0.1.7` / `0.2.x` 走插件
257
276
  `Config`(34 个 GUI 旋钮标记为 `.volatile()`,条目 id `engram`)。浏览器设置卡在两代际
258
277
  都经插件自有 HTTP 路由(`/api/dsh-engram/settings`,见上文 Route B)读写。完整回归
259
- (npm test 329/329,含新增的 volatile-Config 模型 settings-v2 覆盖)在 DSH
278
+ (npm test 396/396,含新增的 volatile-Config 模型 settings-v2 覆盖)在 DSH
260
279
  `0.2.0-rc.2` 依赖集(cordis 4.0.4 + schemastery 3.18.4 + cosmokit,与全局 DSH CLI 同版本)
261
280
  上通过;`.volatile()`/loader 热提交路径与 describe 侧投影已针对真实 `dsh-settings
262
281
  0.2.0-rc.2` 工具函数验证;并在隔离的 `dsh 0.2.0-rc.2` profile 上完成了**真机启动烟测并
@@ -490,22 +509,27 @@ Context GC 的装配在**两个平面**,但两个开关都是 opt-in(默认
490
509
  - **preset 平面(web profile)——显式开启后覆盖全部 stock 预设**:web 面把 compaction 放在 agent
491
510
  preset 的 isolate realm(shipped 预设自挂 `compaction-basic`),host 条目进不去、profile patch 也
492
511
  够不到预设文件(`mountPreset` 的 Include 不带 patches——已对源码核实)。所以插件在启动时
493
- (`agentPresets` 服务就绪后)**自动改写每个仍处于出厂 stock 布局的预设**——默认预设 + 整张 roster
494
- (shipped 根 + `~/.dsh/.agent-presets` 用户根,`standard`/`code`/`cordis` 和你自己的预设都覆盖):
495
- 把 `compaction` 组里的 `compaction-basic` 行换成 `dsh-engram/compaction`。这一自动装配是:
512
+ (`agentPresets` 服务就绪后)**自动改写每个仍处于出厂 stock 布局的预设**——默认预设 + 整张 roster,
513
+ 两代预设布局都覆盖:旧版 `<root>/<id>/agent.cordis.yml` 组合文件(DSH ≤ 0.1.x,行在 4 空格)与
514
+ 声明式 `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml`(DSH ≥ 0.2.0,行在
515
+ `@deepseek-ai/dsh-agent-preset` 声明里、14 空格缩进;没有出厂文件的用户自定义声明式预设不碰):
516
+ 把 `compaction` 组里的 `compaction-basic` 行换成 `dsh-engram/compaction`。行按 YAML 结构定位
517
+ (带 `group: true` 的列表项)、按 `- id:` 与 `name:` 双重校验,**与缩进列数无关**。这一自动装配是:
496
518
  - **默认关(opt-in)**(`autoWebCompaction: false`;设置卡「记忆 GC」可开),开启后幂等——已替换则跳过;
497
519
  **关掉的含义**:只是在之后的启动时不再自动接管(headless/TUI/base 的 host 平面不受影响)——
498
520
  它**不会**还原之前已接管的预设,要还原用 `npm run web-compaction:revert`;
499
521
  - **逐预设判定,只碰 stock 布局**——用户自定义过 compaction 组的预设,以及根本没有 compaction 组的
500
522
  预设(如 shipped `minimal`),一律不写、只记日志;
501
- - **带备份 + 严格校验**:每个文件写前在旁边留 `agent.cordis.yml.engram.bak`(create-only,保留首次
502
- 原件),写后重读校验,任何一步可疑即回滚该文件;改写失败只 warn,该会话仍用 DSH 默认摘要,
523
+ - **带备份 + 严格校验**:每个文件写前在旁边留 `.engram.bak`(create-only,保留首次
524
+ 原件;旧版布局为 `agent.cordis.yml.engram.bak`,声明式布局为 `<id>.patch.yml.engram.bak`),
525
+ 写后重读校验,任何一步可疑即回滚该文件;改写失败只 warn,该会话仍用 DSH 默认摘要,
503
526
  **永不影响宿主**。
504
527
  - **配置真实传导**:写进预设行的引擎配置来自设置卡/配置里的 `gcReplacesCompaction` 与
505
528
  `gcNarrative`——改了这两个开关,下次启动会自动把已接管的行刷新成新配置(幂等重写),
506
529
  所以**设置卡对 web 平面同样生效**,不再硬编码 true/true。
507
- - 手动的完全等价物(预览 / 审计 / 卸载前回归,随 npm 包发布的 CLI,默认扫 shipped + 用户两个根、
508
- 可 `--file` 指定单个):
530
+ - 手动的完全等价物(预览 / 审计 / 卸载前回归,随 npm 包发布的 CLI,自动发现两代预设布局——
531
+ 旧版根、出厂声明式预设与上次启动记录的路径;`--presets-dir <dir>` 覆盖声明式目录探测、
532
+ `--file` 指定单个):
509
533
  ```bash
510
534
  npx dsh-engram status # stock → 未动;wired → 已接管;custom → 不碰
511
535
  npx dsh-engram doctor # status + 按缺口排序的下一步建议
@@ -639,9 +663,20 @@ overview)现在返回 `activation: { boundaryPrune, recall }`:
639
663
  GUI「遥测」页签直接画这块账本(净 token 大数字 + 节省/注入/重写罚金三卡 + 近 14 天红绿柱),
640
664
  记忆概览页也多一张「上下文净 token」卡。落盘在 `~/.dsh/engram/ledger.json`(原子写、按天滚动、
641
665
  delta 落盘),进程内只做整数累加,热路径零 I/O、零模型调用。默认 `ledgerEnabled: true`、
642
- `ledgerCacheDiscount: 0.1`;关掉 = 不挂监听、不写文件。**口径诚实**:token 按
643
- `chars / 4` 估算,不含 provider 实际计费与模型质量差;Context GC 的压缩节省暂未
644
- 接入(宿主未暴露前后大小),因此账本只记「可证明」的部分。
666
+ `ledgerCacheDiscount: 0.1`;关掉 = 不挂监听、不写文件。
667
+
668
+ **节省的两条入账腿**(v0.7.1 补齐,此前只记成本所以净值恒负):
669
+
670
+ 1. **L1 边界剪枝**——`toolResultPruner` 剪掉的 chars,按后续请求数计入(需要有 host
671
+ 平面的 `toolResultPruner`;DSH ≥ 0.2.0 的 web profile 把该行移进 agent preset 的
672
+ isolate realm,本 bundle 的 `cordis.patch.yml` 会重新启用 host 行,状态快照里
673
+ `boundaryPrune.armed` 可核对);
674
+ 2. **压缩(Context GC)**——每次 checkpoint 用「被替换区间可见字符数 − checkpoint 字符数」
675
+ 入账,host 平面与 web 预设域(经 `engramLedger` 服务桥接)走同一个账本。
676
+
677
+ **口径诚实**:token 按 `chars / 4` 估算,不含 provider 实际计费与模型质量差;
678
+ 某工作区还没发生过剪枝/压缩时「节省」就是 0,净值等于注入成本 + 重写罚金(负值正常),
679
+ 遥测页会直接提示这一点。
645
680
 
646
681
  ### JIT 推送(v0.6-C)
647
682
 
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-engram bundle patch: inserts the host plugin row into the web profile
2
2
  # roster. Activate with:
3
- # dsh plugin --profile web add file:/d1/chuxiong/code/dsh-engram
3
+ # dsh plugin --profile web add file:/Users/sunchuxiong/dsh-loom
4
4
  #
5
5
  # The row is a bare host-half plugin by package name. It does not need to
6
6
  # enable storage-domain / storage-json / session-query-sqlite — the standard
@@ -15,14 +15,27 @@
15
15
  # NOTE: with `gcReplacesCompaction:false`, dsh-engram still mounts a plain
16
16
  # BasicCompactionEngine, so compaction never goes away while it is installed.
17
17
  # The host-plane provider is mounted by `lib/index.js` (`mountCompactionEngine`);
18
- # the WEB plane is auto-wired at boot (`autoWebCompaction`, default on): the
19
- # plugin swaps the `compaction-basic` row for `dsh-engram/compaction` in every
20
- # stock-layout preset — default + full roster (shipped + user roots; idempotent,
21
- # backed up, never touches a custom or no-compaction preset; manual CLI
18
+ # the WEB plane is auto-wired at boot when `autoWebCompaction` is on (opt-in
19
+ # since v0.6, default off): the plugin swaps the `compaction-basic` row for
20
+ # `dsh-engram/compaction` in every stock-layout preset — default + full roster
21
+ # (shipped `agent.cordis.yml` roots and DSH ≥ 0.2.0 declarative
22
+ # `dsh-web-app/presets/*.patch.yml`; idempotent, backed up, never touches a
23
+ # custom or no-compaction preset; manual CLI
22
24
  # `npm run web-compaction:status|enable|revert`).
23
25
  - id: compaction-basic
24
26
  disabled: true
25
27
 
28
+ # BoundaryPrune (ResultPack L1) needs DSH's `toolResultPruner` service on the
29
+ # HOST plane. DSH ≥ 0.2.0 web profiles move compaction + the pruner into each
30
+ # agent preset's isolate realm and disable the host-plane `tool-result-pruner`
31
+ # row, so the host plugin's L1 found no service and silently went dormant (the
32
+ # Context Ledger then never credited pruning savings). Re-arm the host row here
33
+ # — the preset realm keeps its own copy for compaction-time pruning, so both
34
+ # paths coexist. Without this, `boundaryPrune` only works on headless/TUI/base
35
+ # profiles. Verify via the status snapshot's `boundaryPrune.armed`.
36
+ - id: tool-result-pruner
37
+ disabled: false
38
+
26
39
  - insert:
27
40
  - id: engram
28
41
  name: dsh-engram
@@ -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
- # 每个文件旁留 agent.cordis.yml.engram.bak)
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` | `true` | 是否在启动时自动改写 stock 预设并接管 web 平面 |
148
- | `gcReplacesCompaction` | `true` | `true`=用 Context GC;`false`=回退 DSH 默认 LLM 摘要(`compaction` 服务永不缺席) |
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
- - 每个被改写文件旁有 `agent.cordis.yml.engram.bak`(create-only,保留首次原件)。
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. FAQ
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 被裁剪也能找回。
@@ -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`
@@ -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,
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