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 +113 -2
- package/README.zh.md +83 -3
- package/cordis.patch.yml +12 -0
- package/lib/api.js +32 -2
- package/lib/client.js +15 -1
- package/lib/compaction.js +85 -0
- package/lib/context-gc.js +626 -0
- package/lib/index-block.js +280 -28
- package/lib/index.js +100 -14
- package/lib/settings.js +4 -0
- package/lib/store.js +31 -0
- package/lib/tools.js +98 -8
- package/lib/trigger.js +379 -0
- package/package.json +15 -2
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
|
-
|
|
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 #
|
|
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 #
|
|
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
|
-
|
|
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 #
|
|
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
|
-
|
|
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
|
+
}
|