dsh-engram 0.7.0 → 0.7.2

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.2`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
268
+ - **最后验证日期 / Last verified**: 2026-10-20 — DSH `0.2.0` live composition (`dsh-context` + `dshmarket` + `dsh-engram`) arms L1 boundary prune; npm test 406/406
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,9 @@ 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` (npm) | published | 同 `0.5.0` — web-plane Context GC on DSH `0.2.0` declarative presets (14-space rows), bundle patch re-enables the host `tool-result-pruner`, compaction savings feed the Context Ledger | 同 `0.5.0` |
287
+ | `0.7.2` (本期 / this release) | prepared | 同 `0.5.0` — L1 boundary prune arms through `ctx.inject`, fixing the boot-ordering race where a pruner activating one tick after the plugin stayed dormant for the whole process | 同 `0.5.0` |
286
288
 
287
289
  ### Where the API family broke — `0.1.2-alpha.2`
288
290
 
@@ -332,7 +334,63 @@ drives mutations).
332
334
  So: keep DSH on `0.1.1-rc.2` or older while running a release ≤ `0.3.6`, and
333
335
  upgrade the plugin to `>=0.3.7` before moving DSH to `0.1.2-alpha.2`.
334
336
 
335
- ### Current release — `0.7.0`
337
+ ### Current release — `0.7.2`
338
+
339
+ **L1 boundary prune was silently dormant on some profiles** — a boot-ordering
340
+ race, not a missing patch.
341
+
342
+ `BoundaryPrune` resolved DSH's `toolResultPruner` with a strict
343
+ `ctx.get("toolResultPruner")` at install time. On DSH `0.2.0` web profiles the
344
+ pruner's fiber activates **~100 ms after** the plugin (measured on the live
345
+ composition `dsh-context` + `dshmarket` + `dsh-engram`: `t0 strict=undefined /
346
+ loose=object`, strict resolves 100 ms later), so the strict lookup was empty,
347
+ `installBoundaryPrune` gave up, and L1 never armed for the life of the process —
348
+ taking the ledger's only prune-credit leg with it. The status snapshot also
349
+ reported that race as "row missing", pointing at the wrong fix.
350
+
351
+ - **Arm through `ctx.inject(["toolResultPruner"], …)`** — cordis runs the
352
+ callback the moment the service becomes active (and again after a reload), so
353
+ arming no longer depends on composition order; the durable status snapshot is
354
+ rewritten as soon as L1 is armed.
355
+ - **"Absent" and "late" now read differently** — `boundaryPruneAvailability`
356
+ falls back to a non-strict lookup: a disabled/missing row still says
357
+ `no toolResultPruner service … (the bundle patch must re-enable …)`, a
358
+ registered-but-inactive one says `registered but its fiber is not active yet`.
359
+ - **Verified** — the live bundle composition (with `dsh-context`) reproduces
360
+ `armed:false` in an isolated `DSH_HOME`; after the fix the same composition
361
+ reports `armed:true · threshold 8192`. `npm test` 406/406.
362
+
363
+ ### Previous release — `0.7.1`
364
+
365
+ Web-plane Context GC on DSH `0.2.0`'s **declarative presets** — the preset
366
+ format changed shape (one `@deepseek-ai/dsh-agent-preset` declaration inside
367
+ `dsh-web-app/presets/*.patch.yml`), and the auto-wiring was anchored to the old
368
+ `agent.cordis.yml` layout, so `autoWebCompaction` silently skipped every preset
369
+ (and could half-rewrite a stock row: the pattern did not include its trailing
370
+ newline).
371
+
372
+ - **Structural row location** — the `compaction` group is found by YAML
373
+ structure (a list item carrying `group: true`), not by column position; the
374
+ stock/engram rows are validated by BOTH their `- id:` and `name:` values, and
375
+ the replacement inherits the row's own indent. Legacy `agent.cordis.yml`
376
+ (4 spaces) and declarative patch files (14/16/18 spaces) both round-trip
377
+ byte-identically.
378
+ - **Declarative discovery** — the preset roster no longer carries `path` on
379
+ DSH `0.2.0`; the plugin maps each roster id to the shipped
380
+ `<presetsDir>/<id>.patch.yml` (resolved through the `dsh` install, with
381
+ `DSH_WEB_PRESETS_DIR` override) and skips user-defined declarative presets
382
+ (profile-patch overrides) with an explicit log line instead of failing.
383
+ - **Status/doctor** — `npx dsh-engram status`/`doctor` discover both preset
384
+ generations, show the detected layout, and flag `autoWebCompaction=false`
385
+ (the opt-in toggle that makes the web plane a no-op), node_modules preset
386
+ wiring (survives `npm update dsh` only while auto-wiring stays on), and
387
+ profile-patch `preset-*` overrides that outrank the shipped files.
388
+ - **Migration** — restart `dsh web` with `autoWebCompaction:true`: the next boot
389
+ re-wires every stock declarative preset (idempotent refresh; existing
390
+ hand-wired rows are rewritten to the requested engine config). `npm test`
391
+ 396/396.
392
+
393
+ ### Previous release — `0.7.0`
336
394
 
337
395
  From event records to **maintainable current truth**: sessions sediment an
338
396
  episode at teardown, new sessions get a resume brief, `esr_node` becomes a
@@ -378,7 +436,7 @@ namespace via `settings.installSection`; on `≥0.1.7` / `0.2.x` the form is the
378
436
  plugin's `Config` with the 34 GUI knobs marked `.volatile()` (entry id
379
437
  `engram`). The browser settings card reads and writes through the plugin's own
380
438
  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
439
+ regression suite (npm test 396/396, incl. the new settings-v2 coverage for the
382
440
  volatile-Config model) passes against the DSH `0.2.0-rc.2` dependency set
383
441
  (cordis 4.0.4 + schemastery 3.18.4 + cosmokit, matching the global DSH CLI).
384
442
  The `.volatile()`/loader hot-commit path and the describe-side projection were
@@ -678,10 +736,15 @@ default):
678
736
  builds its `Include.Config` with no patches; verified against the harness
679
737
  source). So on boot, once the `agentPresets` service is ready, the plugin
680
738
  **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:
739
+ preset plus the whole roster, across BOTH preset generations: legacy
740
+ `<root>/<id>/agent.cordis.yml` compositions (DSH ≤ 0.1.x, row at 4 spaces)
741
+ and the declarative `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml` files
742
+ (DSH ≥ 0.2.0, row at 14 spaces inside the `@deepseek-ai/dsh-agent-preset`
743
+ declaration; a user-defined declarative preset with no shipped file is left
744
+ alone). The row is located by YAML structure (a list item carrying
745
+ `group: true`) and validated by both its `- id:` and `name:` values, so the
746
+ swap is column-position independent: it swaps the `compaction-basic` row
747
+ inside the `compaction` group for `dsh-engram/compaction`. That auto-wiring is:
685
748
  - **off by default (opt-in)** (`autoWebCompaction: false`, toggle in the
686
749
  settings card's memory-GC section); once enabled it is idempotent — an
687
750
  already-wired preset is left alone;
@@ -691,8 +754,9 @@ default):
691
754
  - **per-preset**: only stock-layout presets are touched — a preset with a
692
755
  custom compaction group, or none at all (e.g. the shipped `minimal`
693
756
  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
757
+ - **backed up + validated**: a create-only `.engram.bak`
758
+ (`agent.cordis.yml.engram.bak` or `<id>.patch.yml.engram.bak`) holds each
759
+ original next to its file, every result is re-read and verified
696
760
  after writing, and any doubt rolls that file back; a failure only warns and
697
761
  that session keeps DSH's default summarizer — the host plane is never
698
762
  affected.
@@ -702,7 +766,9 @@ default):
702
766
  changing the settings knobs re-provisions the web rows on the next boot
703
767
  instead of being silently ignored.
704
768
  - A fully equivalent manual CLI, shipped in the npm package
705
- (scans both the shipped and the user root; `--file <path>` targets one):
769
+ (scans both preset generations — legacy roots, the shipped declarative
770
+ presets, and the paths recorded at boot; `--presets-dir <dir>` overrides
771
+ declarative discovery, `--file <path>` targets one):
706
772
  ```bash
707
773
  npx dsh-engram status # stock = untouched · wired = active · custom = hands-off
708
774
  npx dsh-engram doctor # status + ranked next steps for every gap found
@@ -872,10 +938,25 @@ adds a "net context tokens" stat card. Durable rollup at
872
938
  `~/.dsh/engram/ledger.json` (atomic write, daily buckets, delta flush); the hot
873
939
  path is an integer add, zero I/O and zero model calls.
874
940
  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.
941
+ no writes.
942
+
943
+ **Two credit legs** (v0.7.1 — before this the ledger only charged costs, so the
944
+ net was structurally negative):
945
+
946
+ 1. **L1 boundary prune** — chars removed by `toolResultPruner`, credited per
947
+ later request. Requires a host-plane `toolResultPruner`; DSH ≥ 0.2.0 web
948
+ profiles move that row into each agent preset's isolate realm, so the bundle
949
+ patch re-enables the host row — check `boundaryPrune.armed` in the status
950
+ snapshot.
951
+ 2. **Compaction (Context GC)** — every checkpoint credits `span chars −
952
+ checkpoint chars` from the engine that produced it; host plane and the web
953
+ preset realm (bridged through the `engramLedger` service) write the same
954
+ ledger.
955
+
956
+ **Honest scope**: tokens are `chars / 4` estimates, not provider billing, and no
957
+ quality delta is claimed. Until a workspace actually prunes or compacts its
958
+ "saved" leg is 0, so the net equals injected + rewrite penalty (negative is
959
+ expected) — the Telemetry tab says so inline.
879
960
 
880
961
  ### JIT push (v0.6-C)
881
962
 
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.2`;已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` —— 见下文矩阵)
170
+ - **最后验证日期**:2026-10-20 —— DSH `0.2.0` 实机组合(`dsh-context` + `dshmarket` + `dsh-engram`)验证 L1 边界剪枝武装;npm test 406/406 通过
171
171
  - **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
172
172
 
173
173
  > **一句话结论**:所有**已真正发布**的版本(`0.2.0` … `0.3.6`)**只支持旧版
@@ -182,7 +182,9 @@ 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`(npm) | 已发布 | 同 `0.5.0` —— web 平面 Context GC 适配 DSH `0.2.0` 声明式预设(14 空格缩进)、bundle patch 重新启用 host 平面 `tool-result-pruner`、压缩节省接入 Context Ledger | 同 `0.5.0` |
187
+ | `0.7.2`(本期 / this release) | 待发布 | 同 `0.5.0` —— L1 边界剪枝改经 `ctx.inject` 武装,修掉「pruner 行比插件晚激活一帧就永久休眠」的启动时序竞态 | 同 `0.5.0` |
186
188
 
187
189
  ### API 族断裂点:`0.1.2-alpha.2`
188
190
 
@@ -222,7 +224,43 @@ profile 条目 id(`engram`)为命名空间,不再需要任何“挂载”
222
224
  所以:跑 ≤ `0.3.6` 的已发布版本时请把 DSH 保持在 `0.1.1-rc.2` 或更旧;升级
223
225
  DSH 到 `0.1.2-alpha.2` 之前,先升级插件到 `>=0.3.7`。
224
226
 
225
- ### 当前发布版本 —— `0.7.0`
227
+ ### 当前发布版本 —— `0.7.2`
228
+
229
+ 修复 **L1 边界剪枝的启动时序竞态**(真机定位:组合里多挂两个 bundle 就复现)。
230
+
231
+ `BoundaryPrune` 在插件安装时用严格 `ctx.get("toolResultPruner")` 取宿主剪枝器;而 `0.2.0`
232
+ 的 web profile 里,剪枝器的 fiber 比插件**晚激活约 100 ms**(实测:`dsh-context` +
233
+ `dshmarket` + `dsh-engram` 组合下 `t0 strict=undefined / loose=object`,100 ms 后 strict 才
234
+ 拿到服务)。严格查找此时为空 → `installBoundaryPrune` 放弃 → **L1 整个进程生命周期都不武装**,
235
+ 账本唯一的「剪枝节省」入账腿静默断掉;设备快照还会把这种"还没到"报成"行不存在",把人引向错误的排查方向。
236
+
237
+ - **改经 `ctx.inject(["toolResultPruner"], …)` 武装** — cordis 在服务真正激活那一刻回调
238
+ (重载后也会重新武装),不再赌安装顺序;武装成功后立刻重写状态快照。
239
+ - **区分「缺失」与「晚到」** — `boundaryPruneAvailability` 增加非严格查找:行被禁用/没装
240
+ 报 `no toolResultPruner service … (the bundle patch must re-enable …)`,只是 fiber 未激活
241
+ 报 `registered but its fiber is not active yet`。两种原因以前在快照里长得一模一样。
242
+ - **验证** — 用真实 profile 的 bundle 组合(含 `dsh-context`)在隔离 `DSH_HOME` 复现
243
+ `armed:false`,修复后同一组合 `armed:true · threshold 8192`;`npm test` 406/406。
244
+
245
+ ### 上一发布版本 —— `0.7.1`
246
+
247
+ 修复 **DSH `0.2.0` 声明式预设下的 web 平面 Context GC 接管**:旧版自动装配以
248
+ `agent.cordis.yml` 的 4 空格布局为锚,而 `0.2.0` 把预设换成
249
+ `dsh-web-app/presets/<id>.patch.yml` 里的 `@deepseek-ai/dsh-agent-preset` 声明(行在
250
+ 14/16/18 空格缩进),导致 `autoWebCompaction` 静默跳过所有预设。
251
+
252
+ - **结构化定位 compaction 行**:找 `group: true` 的 `compaction` 组,组内按 `- id:` +
253
+ `name:` 双重校验定位 stock/engram 行,替换块继承该行自己的缩进;旧版与声明式布局都
254
+ swap → restore 逐字节还原。顺带修掉旧 stock 替换串不含行尾换行、可能半改写文件的隐患。
255
+ - **声明式预设发现**:roster 不再带 `path` 时把 id 映射到出厂 `<presetsDir>/<id>.patch.yml`
256
+ (`DSH_WEB_PRESETS_DIR` 可覆盖);无出厂文件的用户自定义声明式预设跳过并打日志。
257
+ - **status/doctor 适配**:自动发现两种布局并显示布局类型;doctor 新增
258
+ `autoWebCompaction=false`、node_modules 预设(`npm update dsh` 会恢复出厂布局)、
259
+ profile patch `preset-*` 覆写(优先于 shipped 文件)三类提示。
260
+ - **迁移**:打开 `autoWebCompaction` 后重启一次 `dsh web`,下一次启动即幂等接管所有
261
+ stock 声明式预设;`npm test` 396/396。
262
+
263
+ ### 上一发布版本 —— `0.7.0`
226
264
 
227
265
  把「记忆」从**事件记录**推进到**可维护的当前真相**:会话结束自动沉淀 episode、
228
266
  新会话给恢复简报、`esr_node` 升级为实体页、文件锚点让过期真相可被检测。
@@ -256,7 +294,7 @@ Config** 模型(并带特性检测的旧代际回退)。
256
294
  走 `settings.installSection` 注册 `dsh-engram` 命名空间;`≥0.1.7` / `0.2.x` 走插件
257
295
  `Config`(34 个 GUI 旋钮标记为 `.volatile()`,条目 id `engram`)。浏览器设置卡在两代际
258
296
  都经插件自有 HTTP 路由(`/api/dsh-engram/settings`,见上文 Route B)读写。完整回归
259
- (npm test 329/329,含新增的 volatile-Config 模型 settings-v2 覆盖)在 DSH
297
+ (npm test 396/396,含新增的 volatile-Config 模型 settings-v2 覆盖)在 DSH
260
298
  `0.2.0-rc.2` 依赖集(cordis 4.0.4 + schemastery 3.18.4 + cosmokit,与全局 DSH CLI 同版本)
261
299
  上通过;`.volatile()`/loader 热提交路径与 describe 侧投影已针对真实 `dsh-settings
262
300
  0.2.0-rc.2` 工具函数验证;并在隔离的 `dsh 0.2.0-rc.2` profile 上完成了**真机启动烟测并
@@ -490,22 +528,27 @@ Context GC 的装配在**两个平面**,但两个开关都是 opt-in(默认
490
528
  - **preset 平面(web profile)——显式开启后覆盖全部 stock 预设**:web 面把 compaction 放在 agent
491
529
  preset 的 isolate realm(shipped 预设自挂 `compaction-basic`),host 条目进不去、profile patch 也
492
530
  够不到预设文件(`mountPreset` 的 Include 不带 patches——已对源码核实)。所以插件在启动时
493
- (`agentPresets` 服务就绪后)**自动改写每个仍处于出厂 stock 布局的预设**——默认预设 + 整张 roster
494
- (shipped 根 + `~/.dsh/.agent-presets` 用户根,`standard`/`code`/`cordis` 和你自己的预设都覆盖):
495
- 把 `compaction` 组里的 `compaction-basic` 行换成 `dsh-engram/compaction`。这一自动装配是:
531
+ (`agentPresets` 服务就绪后)**自动改写每个仍处于出厂 stock 布局的预设**——默认预设 + 整张 roster,
532
+ 两代预设布局都覆盖:旧版 `<root>/<id>/agent.cordis.yml` 组合文件(DSH ≤ 0.1.x,行在 4 空格)与
533
+ 声明式 `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml`(DSH ≥ 0.2.0,行在
534
+ `@deepseek-ai/dsh-agent-preset` 声明里、14 空格缩进;没有出厂文件的用户自定义声明式预设不碰):
535
+ 把 `compaction` 组里的 `compaction-basic` 行换成 `dsh-engram/compaction`。行按 YAML 结构定位
536
+ (带 `group: true` 的列表项)、按 `- id:` 与 `name:` 双重校验,**与缩进列数无关**。这一自动装配是:
496
537
  - **默认关(opt-in)**(`autoWebCompaction: false`;设置卡「记忆 GC」可开),开启后幂等——已替换则跳过;
497
538
  **关掉的含义**:只是在之后的启动时不再自动接管(headless/TUI/base 的 host 平面不受影响)——
498
539
  它**不会**还原之前已接管的预设,要还原用 `npm run web-compaction:revert`;
499
540
  - **逐预设判定,只碰 stock 布局**——用户自定义过 compaction 组的预设,以及根本没有 compaction 组的
500
541
  预设(如 shipped `minimal`),一律不写、只记日志;
501
- - **带备份 + 严格校验**:每个文件写前在旁边留 `agent.cordis.yml.engram.bak`(create-only,保留首次
502
- 原件),写后重读校验,任何一步可疑即回滚该文件;改写失败只 warn,该会话仍用 DSH 默认摘要,
542
+ - **带备份 + 严格校验**:每个文件写前在旁边留 `.engram.bak`(create-only,保留首次
543
+ 原件;旧版布局为 `agent.cordis.yml.engram.bak`,声明式布局为 `<id>.patch.yml.engram.bak`),
544
+ 写后重读校验,任何一步可疑即回滚该文件;改写失败只 warn,该会话仍用 DSH 默认摘要,
503
545
  **永不影响宿主**。
504
546
  - **配置真实传导**:写进预设行的引擎配置来自设置卡/配置里的 `gcReplacesCompaction` 与
505
547
  `gcNarrative`——改了这两个开关,下次启动会自动把已接管的行刷新成新配置(幂等重写),
506
548
  所以**设置卡对 web 平面同样生效**,不再硬编码 true/true。
507
- - 手动的完全等价物(预览 / 审计 / 卸载前回归,随 npm 包发布的 CLI,默认扫 shipped + 用户两个根、
508
- 可 `--file` 指定单个):
549
+ - 手动的完全等价物(预览 / 审计 / 卸载前回归,随 npm 包发布的 CLI,自动发现两代预设布局——
550
+ 旧版根、出厂声明式预设与上次启动记录的路径;`--presets-dir <dir>` 覆盖声明式目录探测、
551
+ `--file` 指定单个):
509
552
  ```bash
510
553
  npx dsh-engram status # stock → 未动;wired → 已接管;custom → 不碰
511
554
  npx dsh-engram doctor # status + 按缺口排序的下一步建议
@@ -639,9 +682,20 @@ overview)现在返回 `activation: { boundaryPrune, recall }`:
639
682
  GUI「遥测」页签直接画这块账本(净 token 大数字 + 节省/注入/重写罚金三卡 + 近 14 天红绿柱),
640
683
  记忆概览页也多一张「上下文净 token」卡。落盘在 `~/.dsh/engram/ledger.json`(原子写、按天滚动、
641
684
  delta 落盘),进程内只做整数累加,热路径零 I/O、零模型调用。默认 `ledgerEnabled: true`、
642
- `ledgerCacheDiscount: 0.1`;关掉 = 不挂监听、不写文件。**口径诚实**:token 按
643
- `chars / 4` 估算,不含 provider 实际计费与模型质量差;Context GC 的压缩节省暂未
644
- 接入(宿主未暴露前后大小),因此账本只记「可证明」的部分。
685
+ `ledgerCacheDiscount: 0.1`;关掉 = 不挂监听、不写文件。
686
+
687
+ **节省的两条入账腿**(v0.7.1 补齐,此前只记成本所以净值恒负):
688
+
689
+ 1. **L1 边界剪枝**——`toolResultPruner` 剪掉的 chars,按后续请求数计入(需要有 host
690
+ 平面的 `toolResultPruner`;DSH ≥ 0.2.0 的 web profile 把该行移进 agent preset 的
691
+ isolate realm,本 bundle 的 `cordis.patch.yml` 会重新启用 host 行,状态快照里
692
+ `boundaryPrune.armed` 可核对);
693
+ 2. **压缩(Context GC)**——每次 checkpoint 用「被替换区间可见字符数 − checkpoint 字符数」
694
+ 入账,host 平面与 web 预设域(经 `engramLedger` 服务桥接)走同一个账本。
695
+
696
+ **口径诚实**:token 按 `chars / 4` 估算,不含 provider 实际计费与模型质量差;
697
+ 某工作区还没发生过剪枝/压缩时「节省」就是 0,净值等于注入成本 + 重写罚金(负值正常),
698
+ 遥测页会直接提示这一点。
645
699
 
646
700
  ### JIT 推送(v0.6-C)
647
701
 
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,33 @@ 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 拿不到 host 平面的 `toolResultPruner`(0.2.0 的 host 行被禁),压缩节省此前进不了账本 | 确认状态快照 `boundaryPrune.armed: true`(`npx dsh-engram status` 会打印);压缩在 0.7.1+ 按 `区间字符数 − checkpoint 字符数` 入账 |
199
+ | L1 状态报 `no toolResultPruner service …` 但 `--dump-config` 里行是启用的 | 行存在却被**运行时**关掉(如 `- id: tool-result-pruner, disabled: true` 的 profile override) | 删掉该 override;bundle patch 只负责重新启用出厂行 |
200
+ | L1 状态报 `registered but its fiber is not active yet` | 启动时序:剪枝器 fiber 比插件晚激活(v0.7.2 前会因此永久休眠) | 升到 0.7.2+(改经 `ctx.inject` 武装),然后重启 |
185
201
 
186
202
  ---
187
203
 
188
- ## 9. FAQ
204
+ ## 9. 账本口径(为什么净 token 可能为负)
205
+
206
+ `net = avoided − injected − rewriteChars × (1 − ledgerCacheDiscount)`,三项都按
207
+ `chars / 4` 估算(见 README「Context Ledger」):
208
+
209
+ - **injected**:`[ENGRAM]`/`[RECALL]`/`[JIT]`/`[ESR]` 块每请求重计的注入成本;
210
+ - **avoided**:**已入账**的节省,只有两条腿——L1 边界剪枝(`toolResultPruner` 剪掉的
211
+ chars × 后续请求数)与压缩(每次 checkpoint 的「被替换区间字符数 − checkpoint 字符数」× 后续请求数);
212
+ - **rewrite penalty**:L1 剪枝会击穿 KV 前缀缓存,按 `1 − 缓存折价` 一次性计价;
213
+ 压缩**不额外计**罚金(压缩是宿主自己的动作,默认后端同样会改写前缀,计了会重复)。
214
+
215
+ 所以:**没剪过、没压缩过的会话,节省必然是 0,净值必然是负的**——负数不代表账本坏了,
216
+ 只代表这一段时间只花了钱没省到。状态快照的 `boundaryPrune.armed` 告诉你 L1 是否真的
217
+ 武装;压缩入账要等第一次 checkpoint。
218
+
219
+ ## 10. FAQ
189
220
 
190
221
  - **有损吗?** 锚定细节**无损**(全部可通过工具重取);只有无锚轮次走叙事/截断;所有原文仍留在
191
222
  会话日志里,即使 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`
@@ -0,0 +1,48 @@
1
+ ## dsh-engram v0.7.2
2
+
3
+ 自 v0.7.1 以来:**修复 L1 边界剪枝的启动时序竞态**——此前在部分 profile 上 L1 整个进程
4
+ 生命周期都不武装,账本的「剪枝节省」入账腿静默断掉,而状态快照把它报成"行不存在"。
5
+
6
+ ### 根因(真机定位)
7
+
8
+ `BoundaryPrune` 在插件安装时用**严格** `ctx.get("toolResultPruner")` 取宿主剪枝器,拿不到
9
+ 就 `log.warn` 后放弃。但 DSH `0.2.0` web profile 里 `tool-result-pruner` 行虽然由 bundle patch
10
+ 重新启用,它的 fiber 却比插件**晚激活约 100 ms**:
11
+
12
+ ```
13
+ t0 strict=undefined loose=object ← 服务已注册,fiber 未激活
14
+ inject 100 ms 后 strict=object ← 服务激活
15
+ ```
16
+
17
+ 严格查找在 `t0` 为空 → `installBoundaryPrune` 直接返回 noop → **L1 永久休眠**(本次问题
18
+ 只在组合里多挂 bundle 时出现:最小 profile 里剪枝器恰好先激活,所以此前的真机验证是绿的)。
19
+
20
+ ### 修复
21
+
22
+ - **改经 `ctx.inject(["toolResultPruner"], callback)` 武装**:cordis 在服务真正激活那一刻
23
+ 回调(插件重载后也会重新武装),武装顺序不再由 bundle 组合顺序决定;武装成功后立刻
24
+ 重写持久化状态快照(首刷可能早于剪枝器激活)。
25
+ - **区分「缺失」与「晚到」**:`boundaryPruneAvailability` 增加非严格查找——
26
+ - 行被禁用/没装:`no toolResultPruner service in the host plane (the bundle patch must re-enable the tool-result-pruner row)`
27
+ - 只是 fiber 未激活:`toolResultPruner is registered but its fiber is not active yet (L1 arms when it activates)`
28
+ - **CLI 同步**:`npx dsh-engram status` 的 L1 行直接复用该 reason,未武装时打印「节省不会入账」告警。
29
+
30
+ ### 验证
31
+
32
+ - 把真实 web profile(`dsh-context` + `dshmarket` + `dsh-engram`)克隆到隔离 `DSH_HOME`
33
+ 复现 `armed:false`;换成修复后的代码,同一组合 `armed:true · threshold 8192`。
34
+ `dshmarket` 组合对照组一直 `armed:true`(说明差异来自激活顺序,不是组合内容)。
35
+ - 单测新增「registered-but-inactive 报 pending 而非 missing」一例;`npm test` 406/406,
36
+ `npm run typecheck` 干净。
37
+
38
+ ### 升级步骤
39
+
40
+ ```bash
41
+ npm publish # 或先 npm pack 检查产物
42
+ dsh plugin --profile web add dsh-engram@0.7.2
43
+ # 重启 dsh web 后确认:
44
+ npx dsh-engram status | head -6 # 期望「L1 边界剪枝:已武装(threshold 8192)」
45
+ ```
46
+
47
+ > 升级不会回填历史账本:`avoidedChars` 从 0 开始累计,第一次边界剪枝/checkpoint 之后
48
+ > 「节省 · 剪枝/压缩」才会出现正数。
@@ -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,45 @@ 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
+ * A non-strict lookup separates two failure modes that look identical through a
142
+ * strict `ctx.get`: the row is **absent** (patch missing / disabled) versus
143
+ * **registered but not yet active** (the pruner's fiber activates a tick or two
144
+ * after this plugin on a full profile — L1 arms through `ctx.inject`, so this
145
+ * state resolves itself, while the absent row never will).
146
+ *
147
+ * @param {object} ctx - the host plugin context.
148
+ * @param {object} config - resolved engram config.
149
+ * @returns {{ armed: boolean, reason: string }}
150
+ */
151
+ export function boundaryPruneAvailability(ctx, config) {
152
+ if (config?.boundaryPrune === false) return { armed: false, reason: "disabled by config (boundaryPrune:false)" };
153
+ const pruner = ctx?.get?.("toolResultPruner");
154
+ if (!pruner || typeof pruner.pruneSession !== "function") {
155
+ const registered = ctx?.get?.("toolResultPruner", false);
156
+ if (registered !== undefined && registered !== null) {
157
+ return {
158
+ armed: false,
159
+ reason: "toolResultPruner is registered but its fiber is not active yet (L1 arms when it activates)",
160
+ };
161
+ }
162
+ return {
163
+ armed: false,
164
+ reason: "no toolResultPruner service in the host plane (the bundle patch must re-enable the tool-result-pruner row)",
165
+ };
166
+ }
167
+ const threshold = pruner.config?.thresholdChars;
168
+ return { armed: true, reason: `threshold ${Number.isFinite(threshold) ? threshold : "default"}${config?.boundaryPruneThresholdChars > 0 ? ` (override ${config.boundaryPruneThresholdChars})` : ""}` };
169
+ }
170
+
131
171
  /** Install the boundary-prune listener. Returns a disposer. */
132
172
  export function installBoundaryPrune(ctx, config, log, hooks = {}, opts = {}) {
133
173
  // Default ON (v0.6 default correction): explicit `false` is the opt-out.