dsh-engram 0.3.0 → 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
@@ -33,12 +33,45 @@ with one goal: **save tokens**.
33
33
  `force:true` (or disabling the toggle) skips the disk check, the three gates
34
34
  are still mandatory. The tool and the web forms share one gate
35
35
  (`store.evidenceGate`), so the two surfaces can never drift.
36
+ Tasks are never isolated graph nodes: `esr_task(entity=…)` one-call wiring
37
+ auto-creates the `ent_<slug>` node and hangs the task on it
38
+ (`task --relates_to--> entity`, idempotent), so a task created with a domain
39
+ anchor immediately shows a relation; and `esr_dep` writes the dependency edge
40
+ into both the task's `deps` (blocker logic) and the shared links table, making
41
+ every task dependency a first-class visible edge in the graph.
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.
36
59
  - **Memory GC (pi-esr constraints)** — a scheduled, mechanical, archive-only
37
60
  sweep: TTL-expired memories are archived, over-cap workspaces evict the
38
61
  lowest-value entries, stable tasks past their retention window leave the
39
62
  `[ESR]` surface, and dangling link edges are dropped. The working set (active
40
63
  task refs, task memories, indexed hits) is never touched, and nothing is
41
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.
42
75
  - **Web viewer** — a memory browser with benchmark-ish stats and a config card,
43
76
  built entirely on DSH's native settings slots (no third-party UI package).
44
77
 
@@ -248,9 +281,16 @@ npm run build:client
248
281
  | `esr_task` | Create a task entity (draft → active) | write |
249
282
  | `esr_close` | Close a task via the evidence protocol (artifact + evaluation + memory_ref) | write |
250
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 |
251
286
  | `esr_gc` | Run the memory GC for the workspace (`dry_run:true` previews) | write |
252
287
 
253
- ## 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)
254
294
 
255
295
  A scheduled sweep (`gcIntervalHours`, default 24h) plus a manual `esr_gc` /
256
296
  GUI button keeps the store bounded the pi-esr way — **mechanical, working-set
@@ -268,6 +308,67 @@ GC never touches the working set: memories referenced by an active task
268
308
  is hard-deleted — the report ends with re-fetch pointers for everything it
269
309
  archived, so archives are recoverable, not lost.
270
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
+
271
372
  ## Auto-capture policy
272
373
 
273
374
  Capture is deterministic and offline — it only sees tool *results*, never the
@@ -299,6 +400,11 @@ injected".
299
400
  What the model actually sees (rendered once per session, then frozen):
300
401
 
301
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
+
302
408
  [ENGRAM] workspace: symbolic-index · 2 memories · 1 task(s) active · 0 links
303
409
  [D] 06-18 Decided: use sqlite-vec for retrieval #a2331d87
304
410
  [T] 06-18 Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref #tsk_8b26
@@ -307,6 +413,9 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
307
413
  [ESR] tasks: 1 active / 1 stable
308
414
  - tsk_0d: Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref
309
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
310
419
  ```
311
420
 
312
421
  Prefixes: `[D]` decision · `[E]` error · `[P]` procedure · `[F]` fact ·
@@ -343,6 +452,8 @@ Defaults are token-conscious; override any key via the profile patch
343
452
  gcEnabled: true # scheduled memory GC
344
453
  gcIntervalHours: 24 # sweep cadence
345
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)
346
457
  engramIndexOrder: 40 # systemPrompt section order (before tools band)
347
458
  esrOrder: 41
348
459
  ```
@@ -350,7 +461,7 @@ Defaults are token-conscious; override any key via the profile patch
350
461
  ## Development
351
462
 
352
463
  ```sh
353
- 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)
354
465
  npm run eval # offline recall + structure benchmark (deterministic)
355
466
  npm run build:client
356
467
  ```
package/README.zh.md CHANGED
@@ -23,9 +23,28 @@
23
23
  artifact 按工作区(= 会话 cwd)解析并在磁盘上实存校验,路径不存在则任务保持 ACTIVE、给出原因;
24
24
  `force:true`(或关掉该开关)可跳过磁盘校验——三种证据门照样必填。工具与网页表单共享同一个
25
25
  证据门(`store.evidenceGate`),口径绝不会漂移。
26
+ 任务不再是孤立节点:`esr_task(entity=…)` 一键挂点——自动建 `ent_<slug>` 节点并挂
27
+ `task --relates_to--> entity` 边(幂等),挂着领域锚点的任务立刻带关系;
28
+ `esr_dep` 双写——依赖边同时进 `tasks[].deps`(blocker 逻辑)与 links 表,图谱里每条
29
+ 任务依赖都是一等可见的边。
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。
26
39
  - **记忆 GC(pi-esr 约束)** — 定时、机械、只归档的回收:TTL 过期记忆归档、超容量工作区淘汰低价值条目、
27
40
  stable 任务超保留窗离开 `[ESR]` 表面、悬空链接边清理。工作集(active 任务引用 / 任务记忆 / 已入索引命中)
28
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。
29
48
  - **Web 查看器** — 一个带统计的记忆浏览器和配置卡片,完全构建在 DSH 原生设置槽位上(不碰任何第三方 UI 包)。
30
49
 
31
50
  ```
@@ -162,7 +181,7 @@ npm run build:client
162
181
  ## 测试与评测
163
182
 
164
183
  ```sh
165
- npm test # 29 项单元测试(含 usage 滚动 / /stats 路由)
184
+ npm test # 152 项单元测试(含 usage 滚动 / /stats 路由 / Context GC / ESR 触发)
166
185
  npm run eval # 离线召回 + 结构基准(确定性语料,跑真实 store/recall 路径)
167
186
  ```
168
187
 
@@ -185,9 +204,15 @@ npm run eval # 离线召回 + 结构基准(确定性语料,跑真实 store/
185
204
  | `esr_task` | 创建任务实体(draft → active) | 写 |
186
205
  | `esr_close` | 按证据协议关闭任务(artifact + evaluation + memory_ref) | 写 |
187
206
  | `esr_link` | 在两个实体之间添加类型化关系(迷你图) | 写 |
207
+ | `esr_ready` | 列出可认领任务(无 blocker、无人认领) | 读 |
208
+ | `esr_status` | 拉取实时 ESR 状态 + 派生提示(`since_revision` 增量短响应) | 读 |
188
209
  | `esr_gc` | 运行本工作区的记忆 GC(`dry_run:true` 预览不落库) | 写 |
189
210
 
190
- ## 记忆 GC
211
+ `[ESR]` 块是每会话冻结快照,绝不中途刷新——要最新状态调 `esr_status`。
212
+
213
+ ## GC:两块——记忆面板回收 + Context GC(替代自动 compact)
214
+
215
+ ### 记忆面板回收(存储维护)
191
216
 
192
217
  定时回收(`gcIntervalHours`,默认 24h)+ `esr_gc` 手动触发 + GUI 按钮,按 pi-esr 方式把存储保持在
193
218
  有界内——**机械、工作集保护、只归档**:
@@ -201,6 +226,51 @@ GC 永不触碰工作集:active 任务 `memory_refs` 引用的记忆、task
201
226
  (`hits >= promoteHits`)。用 `esr_gc` + `dry_run: true` 先预览。**不硬删**——报告的末尾为所有
202
227
  归档项附上重取指针,归档可恢复、不是丢失。
203
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
+ ```
204
274
 
205
275
  ## 自动捕获策略
206
276
 
@@ -229,6 +299,11 @@ GC 永不触碰工作集:active 任务 `memory_refs` 引用的记忆、task
229
299
  模型实际看到的内容(每个会话渲染一次,然后冻结):
230
300
 
231
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
+
232
307
  [ENGRAM] workspace: symbolic-index · 2 memories · 1 task(s) active · 0 links
233
308
  [D] 06-18 Decided: use sqlite-vec for retrieval #a2331d87
234
309
  [T] 06-18 Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref #tsk_8b26
@@ -237,6 +312,9 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
237
312
  [ESR] tasks: 1 active / 1 stable
238
313
  - tsk_0d: Retrieval upgrade — ACTIVE · gap: artifact, evaluation, memory_ref
239
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
240
318
  ```
241
319
 
242
320
  前缀:`[D]` 决定 · `[E]` 错误 · `[P]` 流程 · `[F]` 事实 · `[I]` 洞察 · `[H]` 交接 · `[T]` 任务。
@@ -267,6 +345,8 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
267
345
  gcEnabled: true # 定时记忆 GC
268
346
  gcIntervalHours: 24 # 回收节奏
269
347
  gcStableRetentionDays: 120 # 超过此天数的 stable 任务离开 [ESR]
348
+ gcReplacesCompaction: true # Context GC:接管 DSH 自动 compact(false=回退默认 LLM 压缩)
349
+ gcNarrative: true # 无锚轮次走 scoped LLM 叙事;false=纯机械(零 LLM)
270
350
  engramIndexOrder: 40 # systemPrompt section 顺序(位于 tools 段之前)
271
351
  esrOrder: 41
272
352
  ```
@@ -274,7 +354,7 @@ drill: engram_store (user asks to remember) | engram_recall <query> | engram_det
274
354
  ## 开发
275
355
 
276
356
  ```sh
277
- npm test # 21 个测试:核心 + Web API + GC(node:test)
357
+ npm test # 152 个测试:核心 + Web API + GC + Context GC + ESR 触发(node:test)
278
358
  npm run build:client
279
359
  ```
280
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
@@ -442,6 +442,13 @@ export function makeEngramRoutes(service) {
442
442
  });
443
443
  };
444
444
 
445
+ const triggerstats = (_req, res) => {
446
+ const q = readQuery(_req);
447
+ const ws = q.get("workspace");
448
+ const stats = service.recorder?.conversionStats?.(ws && ws.length > 0 ? ws : void 0) ?? null;
449
+ writeJson(res, 200, { stats });
450
+ };
451
+
445
452
  const model = async (req, res) => {
446
453
  const domain = await withDomain(res);
447
454
  if (domain === void 0) return;
@@ -470,21 +477,44 @@ export function makeEngramRoutes(service) {
470
477
  handler: (req, res) => guard(req, res, method, handler),
471
478
  });
472
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
+
473
503
  return [
474
504
  make("GET", "/overview", overview),
475
505
  make("GET", "/memories", memories),
476
- make("GET", "/tasks", tasks),
506
+ makeUnion("/tasks", { GET: tasks, POST: createTask }),
477
507
  make("GET", "/links", links),
478
508
  make("GET", "/nodes", entities),
479
509
  make("GET", "/stats", stats),
480
510
  make("GET", "/toolstats", toolstats),
511
+ make("GET", "/triggerstats", triggerstats),
481
512
  make("GET", "/observations", observations),
482
513
  make("GET", "/model", model),
483
514
  make("GET", "/preview", preview),
484
515
  make("GET", "/config", readConfig),
485
516
  make("POST", "/memories/archive", archive),
486
517
  make("POST", "/memories/delete", remove),
487
- make("POST", "/tasks", createTask),
488
518
  make("POST", "/tasks/close", closeTask),
489
519
  make("POST", "/gc", gcRun),
490
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
+ }