dsh-engram 0.3.2 → 0.3.3

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
@@ -39,30 +39,39 @@ with one goal: **save tokens**.
39
39
  anchor immediately shows a relation; and `esr_dep` writes the dependency edge
40
40
  into both the task's `deps` (blocker logic) and the shared links table, making
41
41
  every task dependency a first-class visible edge in the graph.
42
- - **Decision-point triggers (P0-A/B + P2 + P3)** — firing where the todo→esr
43
- routing decision actually happens and where tasks leak: a `tools/result`
44
- watcher snapshots the native `todo_write` plan and, when the session has ≥ 2
45
- pending todos that outnumber its active ESR tasks, the [ESR] block appends a
46
- concrete `promote:` hint (`esr_task(name="…")`, once per session); recurring
47
- failures (error memories with hits ≥ 2) earn a `root-cause:` task candidate;
48
- a completed plan (pending → 0) earns a `close:` nudge for evidence-complete
49
- tasks; long-unupdated active tasks get a `stale:` nudge; and the task list is
50
- ordered READY-first with a `next: esr_close <id>` line, so the model can act
51
- top-down. The same subscription feeds an in-memory mem-vs-esr counter that
52
- restores `escalationHint` (the per-day `usage` table stopped being written
53
- after assessment P3, silently orphaning the old nudge). Pure rules, zero
54
- LLM, zero per-render I/O. Every hint line carries a stable `#suggest-*`
55
- source tag (traceable in logs) and the same recorder measures hint → esr_*
56
- conversion (shown once per session per hint, attributed when an esr tool
57
- runs within a 10-minute window) — `GET /api/dsh-engram/triggerstats`
58
- reports per-kind shown/converted/rate so you can see which nudge actually
59
- moves behaviour.
42
+ - **ESR trigger mechanism (pi-esr-aligned, hybrid: static protocol + frozen
43
+ snapshot + monotonically accumulated actionables + pull)** — prefix-cache
44
+ stability is the point. Teaching the model WHEN to use ESR is a **static
45
+ methodology** section (byte-identical every turn); the `[ESR]` block is a
46
+ deterministic **per-session frozen snapshot** (set at session start,
47
+ deterministically sorted, timestamps excluded, explicitly "WILL NOT
48
+ auto-refresh") **plus the session's monotonically accumulated actionables**:
49
+ the `promote:` / `root-cause:` / `close:` / `stale:` / `escalate:` lines are
50
+ APPENDED exactly once, the moment each matures, and stay frozen for the rest
51
+ of the session (never removed) — so the prefix changes only when genuinely
52
+ new decision-point info appears, never per-turn, while the timely nudges
53
+ (narrow funnel / recurring failure / work done / stale / balance) stay in
54
+ context. Live state is still **pulled** via `esr_status` (full deterministic
55
+ view + `since_revision` short "unchanged" response + derived actionables).
56
+ The `tools/result` recorder feeds the actionables' data sources; P4
57
+ conversion metering stays (`#suggest-*` tags + 10-minute attribution +
58
+ `GET /api/dsh-engram/triggerstats`). Pure rules, zero LLM.
60
59
  - **Memory GC (pi-esr constraints)** — a scheduled, mechanical, archive-only
61
60
  sweep: TTL-expired memories are archived, over-cap workspaces evict the
62
61
  lowest-value entries, stable tasks past their retention window leave the
63
62
  `[ESR]` surface, and dangling link edges are dropped. The working set (active
64
63
  task refs, task memories, indexed hits) is never touched, and nothing is
65
64
  hard-deleted — every archived entry keeps its id and stays re-fetchable.
65
+ - **Context GC (auto-GC = replaces DSH's auto-compact)** — NOT the memory-panel
66
+ GC. It takes over the host's `compaction` service and swaps DSH's lossy
67
+ LLM-summary compression for **mechanical eviction + re-fetch pointers**:
68
+ evicted turns are scanned for engram/ESR anchors (`#memory-id`, `tsk_*`,
69
+ `ent_*`, `file_path`) and replaced with one line "this detail lives at
70
+ `engram_detail` / `engram_recall` / the [ESR] block"; only un-provenanced
71
+ turns get a scoped LLM narrative (`gcNarrative`, default on, off = fully
72
+ mechanical). All six GC constraints hold (working-set-protected,
73
+ pointer-salience, no-provenance-no-evict…); any error falls back to the
74
+ default compressor — compaction is never broken.
66
75
  - **Web viewer** — a memory browser with benchmark-ish stats and a config card,
67
76
  built entirely on DSH's native settings slots (no third-party UI package).
68
77
 
@@ -272,9 +281,16 @@ npm run build:client
272
281
  | `esr_task` | Create a task entity (draft → active) | write |
273
282
  | `esr_close` | Close a task via the evidence protocol (artifact + evaluation + memory_ref) | write |
274
283
  | `esr_link` | Add a typed relation between two entities (mini graph) | write |
284
+ | `esr_ready` | List claimable tasks (no open blocker, unclaimed) | read |
285
+ | `esr_status` | Pull live ESR state + derived hints (`since_revision` short "unchanged" response) | read |
275
286
  | `esr_gc` | Run the memory GC for the workspace (`dry_run:true` previews) | write |
276
287
 
277
- ## Memory GC
288
+ The `[ESR]` block is a frozen per-session snapshot and never auto-refreshes —
289
+ call `esr_status` for the live truth.
290
+
291
+ ## GC: two planes — memory-panel reclamation + Context GC (replacing auto-compact)
292
+
293
+ ### Memory-panel reclamation (store maintenance)
278
294
 
279
295
  A scheduled sweep (`gcIntervalHours`, default 24h) plus a manual `esr_gc` /
280
296
  GUI button keeps the store bounded the pi-esr way — **mechanical, working-set
@@ -292,6 +308,67 @@ GC never touches the working set: memories referenced by an active task
292
308
  is hard-deleted — the report ends with re-fetch pointers for everything it
293
309
  archived, so archives are recoverable, not lost.
294
310
 
311
+ ### Context GC (replaces DSH's auto-compact)
312
+
313
+ DSH's default context compression is a **lossy LLM full summary**
314
+ (`compaction-basic`): evicted history is crushed into prose — un-queryable, and
315
+ the summary itself burns tokens. dsh-engram's auto-GC replaces it:
316
+ `ContextGcEngine extends BasicCompactionEngine` overrides the single
317
+ `summarize()` hook and swaps the summary body for:
318
+
319
+ 1. **Scan** — the evicted messages are scanned for provenance anchors: memory
320
+ ids echoed by `engram_store`/`engram_recall`/`engram_detail`, `tsk_*`/`ent_*`
321
+ touched by `esr_*`, and `file_path` anchors (`esr_gc` and other management
322
+ calls are NOT anchors);
323
+ 2. **Pointer summary** — every evicted category carries an explicit re-fetch
324
+ call (`engram_detail(id: "…")` / `engram_recall(query)` / the [ESR] block /
325
+ `esr_ready`); the active working set is restated, never evicted;
326
+ 3. **Narrative safety net** — only un-provenanced turns (pure chat / reasoning)
327
+ get a scoped LLM summary (`gcNarrative`, default on; off = the whole path is
328
+ zero-LLM and un-provenanced turns are kept as truncated verbatim).
329
+
330
+ Triggers follow DSH's own compaction timing (step pressure / context-overflow /
331
+ `/compact`); the lock, replay validation, tool-call/result pairing and token
332
+ pricing are all reused from the basic engine. Any error falls back to the
333
+ default compressor — compaction is never broken; with `gcReplacesCompaction:
334
+ false` (or a host without `dsh-compaction-basic`) the engine never mounts.
335
+ Mounting registers the `compaction` service on the plugin's fiber, so
336
+ unloading/reloading engram restores the default engine.
337
+
338
+ **Mounting semantics (verified against cordis source)**: cordis refuses a second
339
+ provider of the same service in the same realm (`provide()` throws
340
+ `service "compaction" has been registered` — not "last write wins"). Hence:
341
+
342
+ - **Host plane**: the plugin patch disables the base `compaction-basic` row
343
+ (re-armed automatically when engram is uninstalled); dsh-engram is then the
344
+ sole `compaction` provider, mounting a bare `BasicCompactionEngine` under
345
+ `gcReplacesCompaction:false` so the service never disappears. Headless / TUI /
346
+ base-style profiles work out of the box.
347
+ - **Preset plane (web profile)**: web keeps compaction inside each agent
348
+ preset's isolated realm (the shipped `standard` preset mounts its own
349
+ `compaction-basic`), unreachable from a host-plane row. Swap that row for the
350
+ mountable entry dsh-engram ships (`dsh-engram/compaction`, package subpath
351
+ export) to activate per session:
352
+ ```yaml
353
+ - id: compaction
354
+ name: cordis:group
355
+ group: true
356
+ isolate:
357
+ compaction: true
358
+ toolResultPruner: true
359
+ config:
360
+ - id: engram-compaction # replaces the original compaction-basic
361
+ name: dsh-engram/compaction
362
+ config:
363
+ gcReplacesCompaction: true # optional; false = default LLM summary for this session
364
+ gcNarrative: true
365
+ - id: command-compact
366
+ name: '@deepseek-ai/dsh-command-compact'
367
+ - id: tool-result-pruner
368
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
369
+ # keep the original config
370
+ ```
371
+
295
372
  ## Auto-capture policy
296
373
 
297
374
  Capture is deterministic and offline — it only sees tool *results*, never the
@@ -323,6 +400,11 @@ injected".
323
400
  What the model actually sees (rendered once per session, then frozen):
324
401
 
325
402
  ```
403
+ ESR operating protocol (static, byte-identical every turn)
404
+ 1. Before starting: esr_ready to see claimable work; esr_status for live state.
405
+ 2. Multi-step work → esr_task (draft first); claim → esr_claim; finish → esr_close (all three gates).
406
+ state is the single source of truth: unsure → call esr_status.
407
+
326
408
  [ENGRAM] workspace: symbolic-index · 2 memories · 1 task(s) active · 0 links
327
409
  [D] 06-18 Decided: use sqlite-vec for retrieval #a2331d87
328
410
  [T] 06-18 Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref #tsk_8b26
@@ -331,6 +413,9 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
331
413
  [ESR] tasks: 1 active / 1 stable
332
414
  - tsk_0d: Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref
333
415
  - closed: tsk_9a (RAG eval) · +1
416
+ snapshot from session start — WILL NOT auto-refresh; call esr_status for live state
417
+ # this-session actionables (frozen)
418
+ promote: 2 pending todo(s) vs 1 ESR task(s) — esr_task(name="…") #suggest-promote
334
419
  ```
335
420
 
336
421
  Prefixes: `[D]` decision · `[E]` error · `[P]` procedure · `[F]` fact ·
@@ -367,6 +452,8 @@ Defaults are token-conscious; override any key via the profile patch
367
452
  gcEnabled: true # scheduled memory GC
368
453
  gcIntervalHours: 24 # sweep cadence
369
454
  gcStableRetentionDays: 120 # stable tasks leave [ESR] after this
455
+ gcReplacesCompaction: true # Context GC: take over DSH auto-compact (false = keep the default LLM summarizer)
456
+ gcNarrative: true # scoped LLM narrative for un-provenanced turns; false = fully mechanical (zero LLM)
370
457
  engramIndexOrder: 40 # systemPrompt section order (before tools band)
371
458
  esrOrder: 41
372
459
  ```
@@ -374,7 +461,7 @@ Defaults are token-conscious; override any key via the profile patch
374
461
  ## Development
375
462
 
376
463
  ```sh
377
- npm test # 29 tests: core + web API + GC + usage-observability (node:test)
464
+ npm test # 152 tests: core + web API + GC + usage-observability + Context GC + ESR triggers (node:test)
378
465
  npm run eval # offline recall + structure benchmark (deterministic)
379
466
  npm run build:client
380
467
  ```
package/README.zh.md CHANGED
@@ -27,19 +27,24 @@
27
27
  `task --relates_to--> entity` 边(幂等),挂着领域锚点的任务立刻带关系;
28
28
  `esr_dep` 双写——依赖边同时进 `tasks[].deps`(blocker 逻辑)与 links 表,图谱里每条
29
29
  任务依赖都是一等可见的边。
30
- - **决策点触发(P0-A/B + P2 + P3)** — 在漏斗最窄处(todo 多、esr 少)与任务泄漏处(建了不关)
31
- 给模型侧触发:监听 `tools/result` 里的原生 `todo_write`,当会话 pending todo ≥ 2 且多于 active ESR
32
- 任务时,`[ESR]` 块追加带具体候选的 `promote:` 提示(`esr_task(name="…")`,每会话仅一次);
33
- 复发失败(error hits≥2)给 `root-cause:` 任务候选;收工(todo 全部完成)给 `close:` 提示关证据齐的
34
- 任务;长期无更新的 active 任务给 `stale:` 提示;活动任务按 READY 优先排序并给 `next: esr_close`。
35
- 并用同一订阅维护进程内 mem-vs-esr 实时计数修复 `escalationHint` 的死数据源(评估 P3 移除了
36
- per-call usage 写路径后原 nudge 已静默失效)。纯规则、零 LLM、零每轮 I/O;每条 nudge 带稳定 `#suggest-*` 来源标记,
37
- 且 recorder 计量「提示曝光 → esr_* 调用」转化(每会话每 kind 记一次曝光,
38
- esr 工具 10 分钟窗口内调用即归因),`GET /api/dsh-engram/triggerstats`
39
- 返回各 kind 的 shown/converted/rate。
30
+ - **ESR 触发机制(pi-esr 对齐,混合:静态协议 + 冻结快照 + 单调渐进 actionables + 拉取)** —
31
+ 前缀缓存稳定是核心。模型「何时用 ESR」靠**静态方法论**(`engram:esr-method`,每轮逐字节相同);
32
+ `[ESR]` 块 = 确定性**冻结快照**(会话开始定下、确定性排序、时间戳排除、明示 WILL NOT
33
+ auto-refresh)+ 本会话**单调渐进的 actionables**:`promote:/root-cause:/close:/stale:/escalate:`
34
+ 在它们成熟的那一刻被**一次追加进块并冻结**(永不回撤)——前缀只在「真正出现新决策点信息」时变化、
35
+ 绝不逐轮漂移,同时保留决策点提醒(漏斗最窄处 / 复发失败 / 收工 / stale / 平衡)。实时状态仍可
36
+ **拉取**:`esr_status`(全量视图 + `since_revision` 增量短响应 + 派生 actionables)。
37
+ recorder 由 `tools/result` 订阅喂入(actionables 的数据源);P4 转化度量照常(`#suggest-*` 标记 +
38
+ 10 分钟归因 + `GET /api/dsh-engram/triggerstats`)。纯规则、零 LLM。
40
39
  - **记忆 GC(pi-esr 约束)** — 定时、机械、只归档的回收:TTL 过期记忆归档、超容量工作区淘汰低价值条目、
41
40
  stable 任务超保留窗离开 `[ESR]` 表面、悬空链接边清理。工作集(active 任务引用 / 任务记忆 / 已入索引命中)
42
41
  永不触碰;**不硬删任何东西**——归档条目保留 id、始终可重取。
42
+ - **Context GC(自动 GC = 替代 DSH 自动 compact)** — 不是记忆面板 GC:接管 DSH 自带的上下文压缩
43
+ (`compaction` 服务),把「有损 LLM 全量摘要」换成**机械驱逐 + 重取指针**——扫描被驱逐轮次里的
44
+ engram/ESR 锚(`#记忆id`、`tsk_*`、`ent_*`、`file_path`),只留一行「细节在 `engram_detail` /
45
+ `engram_recall` / `[ESR]` 里」;只有无锚轮次才走 scoped LLM 叙事兜底(`gcNarrative`,默认开、可关成纯机械)。
46
+ 六条 GC 约束(working-set-protected / pointer-salience / no-provenance-no-evict…)全部落地;任何错误回退
47
+ 默认压缩,永不破坏 compact。
43
48
  - **Web 查看器** — 一个带统计的记忆浏览器和配置卡片,完全构建在 DSH 原生设置槽位上(不碰任何第三方 UI 包)。
44
49
 
45
50
  ```
@@ -176,7 +181,7 @@ npm run build:client
176
181
  ## 测试与评测
177
182
 
178
183
  ```sh
179
- npm test # 29 项单元测试(含 usage 滚动 / /stats 路由)
184
+ npm test # 152 项单元测试(含 usage 滚动 / /stats 路由 / Context GC / ESR 触发)
180
185
  npm run eval # 离线召回 + 结构基准(确定性语料,跑真实 store/recall 路径)
181
186
  ```
182
187
 
@@ -199,9 +204,15 @@ npm run eval # 离线召回 + 结构基准(确定性语料,跑真实 store/
199
204
  | `esr_task` | 创建任务实体(draft → active) | 写 |
200
205
  | `esr_close` | 按证据协议关闭任务(artifact + evaluation + memory_ref) | 写 |
201
206
  | `esr_link` | 在两个实体之间添加类型化关系(迷你图) | 写 |
207
+ | `esr_ready` | 列出可认领任务(无 blocker、无人认领) | 读 |
208
+ | `esr_status` | 拉取实时 ESR 状态 + 派生提示(`since_revision` 增量短响应) | 读 |
202
209
  | `esr_gc` | 运行本工作区的记忆 GC(`dry_run:true` 预览不落库) | 写 |
203
210
 
204
- ## 记忆 GC
211
+ `[ESR]` 块是每会话冻结快照,绝不中途刷新——要最新状态调 `esr_status`。
212
+
213
+ ## GC:两块——记忆面板回收 + Context GC(替代自动 compact)
214
+
215
+ ### 记忆面板回收(存储维护)
205
216
 
206
217
  定时回收(`gcIntervalHours`,默认 24h)+ `esr_gc` 手动触发 + GUI 按钮,按 pi-esr 方式把存储保持在
207
218
  有界内——**机械、工作集保护、只归档**:
@@ -215,6 +226,51 @@ GC 永不触碰工作集:active 任务 `memory_refs` 引用的记忆、task
215
226
  (`hits >= promoteHits`)。用 `esr_gc` + `dry_run: true` 先预览。**不硬删**——报告的末尾为所有
216
227
  归档项附上重取指针,归档可恢复、不是丢失。
217
228
 
229
+ ### Context GC(替换 DSH 自动 compact)
230
+
231
+ DSH 默认的上下文压缩是**有损 LLM 全量摘要**(`compaction-basic`):被驱逐的历史被压缩成散文,
232
+ 不可查询、摘要本身还烧 token。dsh-engram 的自动 GC 替代它:`ContextGcEngine extends
233
+ BasicCompactionEngine` 只重写唯一的 `summarize()` 钩子,把摘要正文换成:
234
+
235
+ 1. **扫描**被驱逐消息的 provenance 锚——`engram_store`/`engram_recall`/`engram_detail` 回显的
236
+ `#记忆id`、`esr_*` 涉及的 `tsk_*`/`ent_*`、`file_path` 锚(`esr_gc` 等管理工具**不算**锚);
237
+ 2. **指针摘要**:每条被驱逐类别都带显式重取调用(`engram_detail(id: "…")` / `engram_recall(query)`
238
+ / `[ESR]` 块 / `esr_ready`),active 工作集在摘要里**复述**不驱逐;
239
+ 3. **兜底叙事**:只有无锚轮次(纯对话/推理)才走 scoped LLM 摘要(`gcNarrative`,默认开;关掉后整条
240
+ 路径零 LLM,无锚轮次截断原文保留)。
241
+
242
+ 触发时机完全跟随 DSH 现有 compact(step pressure / context-overflow / `/compact`);锁、回放校验、
243
+ tool-call/result 配对、token 定价全部复用基本引擎。任何错误 → 回退默认压缩,**永不破坏 compact**。
244
+ 装配即注册 `compaction` 服务,卸载/reload engram 自动还回默认引擎。
245
+
246
+ **装配语义(已按 cordis 源码验证)**:cordis 拒绝同域第二个同名 provider(`provide()` 抛
247
+ `service "compaction" has been registered`,不是「后注册 wins」)。因此:
248
+
249
+ - **host 平面**:插件 patch 直接 `disable` 基座 `compaction-basic` 行(卸载 engram 自动还原),由
250
+ dsh-engram 独占 `compaction`;`gcReplacesCompaction:false` 时仍挂裸 `BasicCompactionEngine`,
251
+ 服务永不缺席。headless / TUI / base 型 profile 开箱即用。
252
+ - **preset 平面(web profile)**:web 面把 compaction 放在 agent preset 的 isolate realm(`standard`
253
+ 预设自挂 `compaction-basic`),host 条目进不去。把 preset `compaction` 组里的 `compaction-basic`
254
+ 行换成 dsh-engram 提供的可挂载条目即可按 session 生效:
255
+ ```yaml
256
+ - id: compaction
257
+ name: cordis:group
258
+ group: true
259
+ isolate:
260
+ compaction: true
261
+ toolResultPruner: true
262
+ config:
263
+ - id: engram-compaction # ← 替换原 compaction-basic
264
+ name: dsh-engram/compaction
265
+ config:
266
+ gcReplacesCompaction: true # 可选;false=该 session 用默认 LLM 摘要
267
+ gcNarrative: true
268
+ - id: command-compact
269
+ name: '@deepseek-ai/dsh-command-compact'
270
+ - id: tool-result-pruner
271
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
272
+ # 原有配置保留
273
+ ```
218
274
 
219
275
  ## 自动捕获策略
220
276
 
@@ -243,6 +299,11 @@ GC 永不触碰工作集:active 任务 `memory_refs` 引用的记忆、task
243
299
  模型实际看到的内容(每个会话渲染一次,然后冻结):
244
300
 
245
301
  ```
302
+ ESR 操作协议(静态,每轮逐字节相同)
303
+ 1. 动工前:esr_ready 看可认领工作;esr_status 拿实时状态。
304
+ 2. 多步工作 → esr_task(draft 起步);动工 → esr_claim;收工 → esr_close(三证齐)。
305
+ state 是唯一真相:拿不准 state 就 call esr_status。
306
+
246
307
  [ENGRAM] workspace: symbolic-index · 2 memories · 1 task(s) active · 0 links
247
308
  [D] 06-18 Decided: use sqlite-vec for retrieval #a2331d87
248
309
  [T] 06-18 Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref #tsk_8b26
@@ -251,6 +312,9 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
251
312
  [ESR] tasks: 1 active / 1 stable
252
313
  - tsk_0d: Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref
253
314
  - closed: tsk_9a (RAG eval) · +1
315
+ snapshot from session start — WILL NOT auto-refresh; call esr_status for live state
316
+ # this-session actionables (frozen)
317
+ promote: 2 pending todo(s) vs 1 ESR task(s) — esr_task(name="…") #suggest-promote
254
318
  ```
255
319
 
256
320
  前缀:`[D]` 决定 · `[E]` 错误 · `[P]` 流程 · `[F]` 事实 · `[I]` 洞察 · `[H]` 交接 · `[T]` 任务。
@@ -281,6 +345,8 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
281
345
  gcEnabled: true # 定时记忆 GC
282
346
  gcIntervalHours: 24 # 回收节奏
283
347
  gcStableRetentionDays: 120 # 超过此天数的 stable 任务离开 [ESR]
348
+ gcReplacesCompaction: true # Context GC:接管 DSH 自动 compact(false=回退默认 LLM 压缩)
349
+ gcNarrative: true # 无锚轮次走 scoped LLM 叙事;false=纯机械(零 LLM)
284
350
  engramIndexOrder: 40 # systemPrompt section 顺序(位于 tools 段之前)
285
351
  esrOrder: 41
286
352
  ```
@@ -288,7 +354,7 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
288
354
  ## 开发
289
355
 
290
356
  ```sh
291
- npm test # 21 个测试:核心 + Web API + GC(node:test)
357
+ npm test # 152 个测试:核心 + Web API + GC + Context GC + ESR 触发(node:test)
292
358
  npm run build:client
293
359
  ```
294
360
 
package/cordis.patch.yml CHANGED
@@ -5,6 +5,18 @@
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
7
7
  # web profile (dsh-base x dsh-web-app) already mounts them.
8
+
9
+ # dsh-engram REPLACES the host-plane auto-compact: its Context GC engine
10
+ # provides the `compaction` service (see docs/PROPOSAL-contextgc.zh.md).
11
+ # cordis refuses two sibling rows registering the same service key, so the
12
+ # base row must be disabled here — this patch layer wins because bundle patch
13
+ # layers apply in bundle order and dsh-engram loads after dsh-base. Removing
14
+ # dsh-engram from the profile re-arms the base row automatically.
15
+ # NOTE: with `gcReplacesCompaction:false`, dsh-engram still mounts a plain
16
+ # BasicCompactionEngine, so compaction never goes away while it is installed.
17
+ - id: compaction-basic
18
+ disabled: true
19
+
8
20
  - insert:
9
21
  - id: engram
10
22
  name: dsh-engram
package/lib/api.js CHANGED
@@ -477,10 +477,33 @@ export function makeEngramRoutes(service) {
477
477
  handler: (req, res) => guard(req, res, method, handler),
478
478
  });
479
479
 
480
+ /** One exact route serving several methods on the same path. The host
481
+ webServer's exact table is keyed by pathname only — two separate routes
482
+ on `/tasks` (GET + POST) collide and the second register() throws
483
+ "duplicate route", which the mount silently skips. So a shared path must
484
+ be a single route that dispatches on req.method itself. */
485
+ const makeUnion = (path, byMethod) => {
486
+ const methods = Object.keys(byMethod);
487
+ return {
488
+ kind: "exact",
489
+ path: `${API_PREFIX}${path}`,
490
+ method: methods,
491
+ handler: (req, res) => {
492
+ const m = String(req.method ?? "GET").toUpperCase();
493
+ const inner = byMethod[m];
494
+ if (inner === void 0) {
495
+ writeError(res, 405, "method not allowed");
496
+ return;
497
+ }
498
+ return guard(req, res, m, inner);
499
+ },
500
+ };
501
+ };
502
+
480
503
  return [
481
504
  make("GET", "/overview", overview),
482
505
  make("GET", "/memories", memories),
483
- make("GET", "/tasks", tasks),
506
+ makeUnion("/tasks", { GET: tasks, POST: createTask }),
484
507
  make("GET", "/links", links),
485
508
  make("GET", "/nodes", entities),
486
509
  make("GET", "/stats", stats),
@@ -492,7 +515,6 @@ export function makeEngramRoutes(service) {
492
515
  make("GET", "/config", readConfig),
493
516
  make("POST", "/memories/archive", archive),
494
517
  make("POST", "/memories/delete", remove),
495
- make("POST", "/tasks", createTask),
496
518
  make("POST", "/tasks/close", closeTask),
497
519
  make("POST", "/gc", gcRun),
498
520
  ];
package/lib/client.js CHANGED
@@ -329,6 +329,7 @@ function EngramTaskDock({ sessionId, useSessions, useWorkspaces, useProjection,
329
329
  const [expandedId, setExpandedId] = (0, import_react2.useState)(null);
330
330
  const [taskPage, setTaskPage] = (0, import_react2.useState)(0);
331
331
  const [promoted, setPromoted] = (0, import_react2.useState)(/* @__PURE__ */ new Set());
332
+ const [promoteNote, setPromoteNote] = (0, import_react2.useState)(null);
332
333
  const [promoteBusy, setPromoteBusy] = (0, import_react2.useState)(false);
333
334
  const [toolStats, setToolStats] = (0, import_react2.useState)(null);
334
335
  const [closeArtifact, setCloseArtifact] = (0, import_react2.useState)("");
@@ -456,18 +457,26 @@ function EngramTaskDock({ sessionId, useSessions, useWorkspaces, useProjection,
456
457
  const promoteAll = async () => {
457
458
  if (!effWs || promoteBusy || toPromote.length === 0) return;
458
459
  setPromoteBusy(true);
460
+ setPromoteNote(null);
459
461
  const done = [];
462
+ const failed = [];
460
463
  for (const it of toPromote) {
461
464
  try {
462
465
  await api.createTask(effWs, it.content, "\u6E90\u81EA\u4F1A\u8BDD\u8BA1\u5212\uFF08todo_write \u4E00\u952E\u6C89\u6DC0\uFF09");
463
466
  done.push(it.content);
464
467
  } catch {
468
+ failed.push(it.content);
465
469
  }
466
470
  }
467
471
  if (done.length > 0) {
468
472
  setPromoted((prev) => /* @__PURE__ */ new Set([...prev, ...done]));
469
473
  void load();
470
474
  }
475
+ if (failed.length > 0) {
476
+ setPromoteNote(
477
+ failed.length === done.length ? `\u6C89\u6DC0\u5931\u8D25\uFF1A${failed.length} \u9879\u672A\u80FD\u521B\u5EFA\uFF08\u5DE5\u4F5C\u533A\u5DF2\u6EE1\u6216\u670D\u52A1\u4E0D\u53EF\u8FBE\uFF09\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5` : `\u90E8\u5206\u5931\u8D25\uFF1A${failed.length}/${toPromote.length} \u9879\u672A\u80FD\u521B\u5EFA`
478
+ );
479
+ }
471
480
  setPromoteBusy(false);
472
481
  };
473
482
  const submitClose = async (task) => {
@@ -726,6 +735,10 @@ function EngramTaskDock({ sessionId, useSessions, useWorkspaces, useProjection,
726
735
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "ed-btn", style: { ...btn, ...btnSolid, padding: "3px 9px", fontSize: 11.5 }, disabled: promoteBusy || toPromote.length === 0, onClick: () => void promoteAll(), title: "\u628A\u672C\u8F6E\u5F85\u529E\u8F6C\u6210 ESR \u4EFB\u52A1\u8349\u7A3F\uFF08\u8DE8\u4F1A\u8BDD\u4FDD\u7559\uFF09", children: promoteBusy ? "\u2026" : promoted.size > 0 ? toPromote.length > 0 ? `\u5DF2\u6C89\u6DC0 ${promoted.size} \u9879 \xB7 \u518D\u6C89\u6DC0 ${toPromote.length} \u9879` : `\u5DF2\u6C89\u6DC0 ${promoted.size} \u9879` : `\u6C89\u6DC0\u5230 ESR\uFF08${toPromote.length} \u9879\uFF09` }),
727
736
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 10.5, color: "var(--dsw-alias-label-dimmed, var(--dsh-color-muted-weak, #9ca3af))" }, children: "\u8F6C\u8349\u7A3F \xB7 \u8DE8\u4F1A\u8BDD\u95ED\u73AF" })
728
737
  ] }),
738
+ promoteNote && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, color: "#dc2626", lineHeight: 1.4 }, role: "alert", children: [
739
+ "\u26A0 ",
740
+ promoteNote
741
+ ] }),
729
742
  activeTasks.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "flex-start", fontSize: 11.5, lineHeight: 1.5, color: "var(--dsw-alias-label-dimmed, var(--dsh-color-muted, #6b7280))", paddingTop: 2 }, children: [
730
743
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": true, style: { color: badgeEsr.color }, children: "\u21B3" }),
731
744
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
@@ -3330,10 +3343,11 @@ var ADVANCED_GROUPS = [
3330
3343
  {
3331
3344
  id: "retention-adv",
3332
3345
  title: "\u751F\u547D\u5468\u671F\u4E0E GC\uFF08\u9AD8\u7EA7\uFF09",
3333
- description: "\u5BB9\u91CF\u4E0A\u9650\u4E0E\u5B9A\u671F\u56DE\u6536",
3346
+ description: "\u5BB9\u91CF\u4E0A\u9650\u4E0E\u5B9A\u671F\u56DE\u6536\uFF08\u81EA\u52A8 GC = \u66FF\u4EE3 DSH \u81EA\u52A8 compact\uFF09",
3334
3347
  fields: [
3335
3348
  { key: "maxMemoriesPerWorkspace", label: "\u5DE5\u4F5C\u533A\u8BB0\u5FC6\u4E0A\u9650", kind: "num", min: 0, max: 1e4 },
3336
3349
  { key: "gcEnabled", label: "\u8BB0\u5FC6 GC", hint: "\u5B9A\u65F6\u56DE\u6536\uFF08\u8FC7\u671F/\u8D85\u5BB9\u91CF/stable \u8D85\u7A97/\u60AC\u7A7A\u94FE\u63A5\uFF09", kind: "bool" },
3350
+ { key: "gcNarrative", label: "Context GC \u53D9\u4E8B\u515C\u5E95", hint: "\u81EA\u52A8 compact \u65F6\u65E0\u951A\u8F6E\u6B21\u7528 LLM \u6458\u8981\uFF1B\u5173\u6389=\u7EAF\u673A\u68B0\uFF08\u96F6 LLM\uFF09", kind: "bool" },
3337
3351
  { key: "gcStableRetentionDays", label: "stable \u4EFB\u52A1\u4FDD\u7559\uFF08\u5929\uFF09", hint: "\u8D85\u7A97\u540E\u7531 GC \u5F52\u6863\u3001\u79BB\u5F00 [ESR] \u8868\u9762", kind: "num", min: 0, max: 3650 },
3338
3352
  { key: "verifyArtifact", label: "\u6821\u9A8C artifact \u5B58\u5728", hint: "esr_close \u7684 artifact \u987B\u5728\u5DE5\u4F5C\u533A\u78C1\u76D8\u4E0A\u771F\u5B9E\u5B58\u5728\uFF08URL \u8C41\u514D\uFF09\uFF1Bforce:true/\u5173\u6389\u53EF\u8DF3\u8FC7", kind: "bool" }
3339
3353
  ]
@@ -0,0 +1,85 @@
1
+ /**
2
+ * dsh-engram — Context GC as a mountable HOST plugin entry (agent-preset row).
3
+ *
4
+ * Web/agent-preset compositions keep compaction on the AGENT plane: the
5
+ * `standard` preset (and friends) mount `@deepseek-ai/dsh-compaction-basic`
6
+ * inside a `compaction` group carrying its own isolate realm. A host-plane row
7
+ * cannot reach that realm, so replacing the default summarizer per-session
8
+ * needs a row that can be listed IN the preset's `compaction` group:
9
+ *
10
+ * ```yaml
11
+ * - id: compaction
12
+ * name: cordis:group
13
+ * group: true
14
+ * isolate:
15
+ * compaction: true
16
+ * toolResultPruner: true
17
+ * config:
18
+ * - id: engram-compaction
19
+ * name: dsh-engram/compaction # this entry
20
+ * - id: command-compact
21
+ * name: '@deepseek-ai/dsh-command-compact'
22
+ * ...
23
+ * ```
24
+ *
25
+ * Config (all optional): `gcReplacesCompaction` (true → Context GC; false →
26
+ * plain BasicCompactionEngine), `gcNarrative`, `gcNarrativeMaxTokens`,
27
+ * `gcNarrativeMaxChars`. Values default to the plugin DEFAULTS.
28
+ *
29
+ * The engine reads engram/ESR state through the storage-domain service shared
30
+ * with the host plane (best-effort reads for pointer classification — the
31
+ * entry never writes).
32
+ */
33
+ import { openEngramDomain } from "./store.js";
34
+ import { loadCompactionEngine, collectPointerContext } from "./context-gc.js";
35
+ import { workspaceKey as wk } from "./util.js";
36
+
37
+ export const name = "dsh-engram-compaction";
38
+ export const inject = ["llm", "tokenMeter", "sessions"];
39
+
40
+ /** One cached domain per storage-domain service (read-only for compaction). */
41
+ const domains = new WeakMap();
42
+
43
+ async function domainFor(ctx) {
44
+ const storageDomain = ctx.get("storageDomain");
45
+ if (!storageDomain) return null;
46
+ let domain = domains.get(storageDomain);
47
+ if (!domain) {
48
+ domain = await openEngramDomain(storageDomain).catch(() => null);
49
+ if (domain) domains.set(storageDomain, domain);
50
+ }
51
+ return domain;
52
+ }
53
+
54
+ export function apply(ctx, config = {}) {
55
+ const mode = config.gcReplacesCompaction === false ? "default" : "context-gc";
56
+ const deps =
57
+ mode === "context-gc"
58
+ ? {
59
+ narrativeEnabled: config.gcNarrative !== false,
60
+ narrativeMaxTokens: config.gcNarrativeMaxTokens ?? 1024,
61
+ narrativeMaxChars: config.gcNarrativeMaxChars ?? 4000,
62
+ readWorkspace: async (workspace, report) => {
63
+ if (!workspace) return null;
64
+ const domain = await domainFor(ctx);
65
+ if (!domain) return null;
66
+ return collectPointerContext(domain, wk(workspace), report);
67
+ },
68
+ }
69
+ : {};
70
+
71
+ return loadCompactionEngine(ctx, { auto: true }, deps, { mode })
72
+ .then((Engine) => {
73
+ if (!Engine) {
74
+ ctx.logger?.warn?.("engram context-gc: dsh-compaction-basic unavailable — no compaction engine");
75
+ return;
76
+ }
77
+ new Engine();
78
+ ctx.logger?.info?.(
79
+ `engram context-gc: compaction = ${mode === "context-gc" ? "Context GC (mechanical eviction + re-fetch pointers)" : "default LLM summarizer (gcReplacesCompaction:false)"}`,
80
+ );
81
+ })
82
+ .catch((error) => {
83
+ ctx.logger?.warn?.(`engram context-gc mount failed: ${String(error)}`);
84
+ });
85
+ }