dsh-engram 0.3.3 → 0.3.5

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.
@@ -0,0 +1,108 @@
1
+ # dsh-engram 方案候选 · 借自 agentmemory(会话去重 / retention / 自动遗忘)
2
+
3
+ > 依据:[rohitg00/agentmemory](https://github.com/rohitg00/agentmemory) 深度研究(TS + iii engine 的
4
+ > hook 驱动记忆流水线:54 MCP 工具 / 12 hooks / 0 外部 DB / 1600+ 测试)。
5
+ > **状态:已归档(研究报告 + 候选方案),未实施。** 实施与否待与用户确认。
6
+ > 约束:全部**纯规则 / 零向量 / 零 LLM 蒸馏 / 确定性单测 / host 等自然重启**。
7
+
8
+ ---
9
+
10
+ ## 研究报告摘要(详见对话经过与仓库)
11
+
12
+ agentmemory = 「coding agent 的持久记忆全流水线」:PostToolUse 自动捕获 → SHA-256 5min 去重 →
13
+ 隐私过滤 → LLM 压缩 → 向量+BM25+知识图谱三索引 → SessionStart 按 token 预算注入。
14
+
15
+ 核心模型:
16
+ 1. **四层记忆巩固**(Working / Episodic / Semantic / Procedural),Ebbinghaus 遗忘曲线。
17
+ 2. **Retention 数学**:`min(1, salience·exp(-λ·Δt) + σ·Σ(1/daysSinceAccess))`;
18
+ access-tracker 记 count/lastAt/recent[≤20];hot/warm/cold 分级。
19
+ 3. **Auto-forget**:per-memory `forgetAfter` TTL、矛盾检测(0.9)、重要性逐出。
20
+ 4. **检索**:BM25+向量+图谱 → RRF(k=60) 融合 + **session-diversified(每会话≤3 条)**;
21
+ superseded 版本移出检索路径;近重复 advisory `similarTo`。
22
+ 5. **关系词汇**:`supersedes / extends / derives / contradicts / related`。
23
+
24
+ ## 三个纯规则候选点(未实施)
25
+
26
+ | # | 候选 | 现状差距 | 落点 | 成本 |
27
+ |---|---|---|---|---|
28
+ | ① | **会话去重 recall**(session-diversified,每会话≤3) | auto-capture 单会话霸榜、他会话高相关被挤掉 | engram_recall 排序后按 sessionId 稳定去重,默认每会话 3 条(可配) | host ~25 行 |
29
+ | ② | **Retention 分数**(Ebbinghaus + 访问强化) | 现有 recency 仅单指数 `1+0.5·exp(-age/14)`,无访问增强 | 新增 `lib/retention.js` 纯函数;记忆加 `lastAccessAt`+`accessWindow`(recall 命中即记);bm25Rank recency 升级为 retention(evidenceBoost 保留);GC 逐出按 retention 排序 | host ~120 行 + GC 调整 |
30
+ | ③ | **Per-memory TTL + 显式 supersedes/contradicts** | 只有全局 expireDays;观察反证是计数而非可回查关系 | `storeMemory` 支持 `expiresAfter`(error 类默认短 TTL);esr_link 记忆层加 `contradicts`/`supersedes` 关系,superseded 检索降权 | host ~100 行 |
31
+
32
+ 明确不做:embedding / MiniLM、图谱抽取、LLM 压缩、secrets 深度脱敏、coordination、git snapshots、
33
+ CJK segmenter。
34
+
35
+ ---
36
+
37
+ ## 外部实证(2026-08-22,AML 首届学术榜头部系统深挖 7 仓库 + 双 bench 实测)
38
+
39
+ 来源:`research/aml_top_systems/SYNTHESIS.md`;实测:`research/personamem_bench/REPORT.md`、
40
+ `research/realdata_bench/REPORT.md`。
41
+
42
+ ### 候选②(retention / recency)——实证修正
43
+ - 头部系统一致做法:recency 用**候选集内相对归一**(episodic:`+= weight*best*relative` 加法且刻意
44
+ 轻微,注释"过重毁旧事实召回");Chrono/FlowGrid 均**意图门控**——仅查询带时间意图(now/current/
45
+ latest/recent/最近/目前…)才放大,且明确"只解近并列、不压过强词法证据"。
46
+ - 我们的双 bench 实测(rank.mjs 加 `recency-gated`/`full-gated` 变体):
47
+ - PersonaMem 32k:gated 30.2% R@5 < 无条件 32.1%(585 问句中 75% 无时间词,gold 铆会话尾部,
48
+ 门控主动放弃数据红利,此语料无条件更优)。
49
+ - 真实库 self-recall:entity 查询全无时间词 → gated == plain(23.0%),无条件 +1.6pp(24.6)——
50
+ **同 entity 记忆里 updatedAt 更新的更接近"当前应记住的状态",实体查询隐含最新语义**。
51
+ - **结论**:对 dsh-engram(agent 工作记忆、实体/主题查询为主),**保持现状轻微无条件 recency
52
+ (lib/util.js 现公式:14 天 +18%、60 天 +1%)优于意图门控**;意图门控+放大仅适合"高频模板化
53
+ 重复陈述"的对话记忆场景,若做则作为配置项(默认 off)。候选②的改动重心应从"改 recency 公式"
54
+ 移到 **GC 逐出按 retention 排序 + lastAccessAt 访问强化**(保留不破坏现状的确定性收益面)。
55
+
56
+ ### 候选③(TTL + supersedes/contradicts)——实证给出门控规格
57
+ - FlowGrid #8 的超前实现与消融(关键实证):
58
+ - 话题签名 containment(交集/较短者)≥0.5 + 时间更晚 → 新者 +4/旧者 −1;
59
+ 链中间态不加不减;**只在查询含时间意图时生效**。
60
+ - **裸结构判定净负**(temporal|paraphrase +0.10,整体 MRR −0.026~−0.034):同话题无关后续提及
61
+ 会误判为覆写;`require_update_cue`(新记录须带"改为/不再/replaced by/supersedes/最新口径…")
62
+ 是修正方向,官方默认 disabled。
63
+ - 真实库情况:同 entity containment≥0.5 有 27 对、带 update-cue 仅 4 对,且多为"同一事件回顾"
64
+ 而非"新状态替换旧状态" → 当前积累下无充分场景,原型无统计意义。
65
+ - **结论**:候选③若实施,规格 = temporal-intent 门控 + require_update_cue + 链中间态中性 +
66
+ **默认关、按配置开启**(FlowGrid 官方即默认 disabled)。在其产生可感知收益前(真实库积累更多
67
+ 更新型记忆),优先级低于候选①②。
68
+
69
+ ### 白捡机制(成本低,待并入候选或另立小项)
70
+ - 会话/实体二级 RRF(Refind:`1/(60+单条rank)+1/(60+会话rank)`)——有 sessionId/entity,可直接做。
71
+ - 相邻证据加成(FlowGrid:同会话 seq±1 也在候选 → +6;Chrono context 通道)。
72
+ - 日期/数字精确命中加成(FlowGrid W_DATE=45 / W_NUMBER=25 同级)。
73
+ - 时间戳/role 烘焙进证据文本(episodic,表示层,我们记忆已带 createdAt 可复用到 evidence 视图)。
74
+
75
+ ---
76
+
77
+ ## AML 编程榜启示与实施顺序(2026-08-22 追加)
78
+
79
+ 来源:`research/AML-coding-2026-08-21.md` + `research/aml-coding-2026-08-21.json`(58 条全量)。
80
+
81
+ ### 榜单事实
82
+ - academic coding 43 条:**峰值 52.67 = 官方 `aml-memory-baseline` 自身**(8 个并列,疑似
83
+ baseline 模板克隆),**0 个提交超过**;其余 35 条在 46.00–52.33。industry 峰值 MemoraX 62.00
84
+ (返回量全榜最小 ~6K、搜索最快 1.8s)。官方对照 `no-memory-8000`(无记忆+8k 上下文)数值仅在网页端。
85
+ - 词法/简单方法全程不落下风:SQLite-FTS 50.00、Refind 50.00、FlowGrid 49.33 vs Mem0 48.67、
86
+ Hindsight 46.67;returnSize 与解决率无单调关系,但「返回极少+快」的 MemoraX 登顶、几乎不返回的
87
+ 反而垫底。
88
+
89
+ ### 对三候选的启示(证据纪律 > 检索魔法)
90
+ 1. **① 会话去重 recall 升为最高优先级**:coding 榜最强提交「返回极少而准」+ textual 榜 RRF/去噪
91
+ 惯例 →「少而准」是当前评测范式下的正收益;engram_recall 的会话去重直接落实这一纪律,成本最低。
92
+ 2. **返回策略做成显式约束**:默认 topK/返回大小可控;证据带 provenance/时间/状态(episodic 烘焙
93
+ 思路),服务下游"判断新旧",这是③的治理面,比 supersede 判定更优先。
94
+ 3. **② retention 保持轻量化**:不改 recency 公式(实测无条件轻微更优);重心在 GC 逐出按
95
+ retention 排序 + lastAccessAt 访问强化(确定性收益面)。
96
+ 4. **③ supersedes 维持门控规格且默认关**:真实库场景不足(27 对候选/4 对 cue),等积累;实施时
97
+ 按 FlowGrid 门控规格(temporal-intent 门控 + require_update_cue + 链中间态中性)。
98
+ 5. **不做向量/重排/复杂融合**:编码榜证明简单方法与复杂系统同带,增强预算花在证据组织上。
99
+
100
+ ### 实施顺序(更新)
101
+ | 序 | 项 | 状态 |
102
+ |---|---|---|
103
+ | 1 | ① 会话去重 recall(每会话≤3,可配)| **下一实施项** |
104
+ | 2 | 返回控制 + 证据字段(provenance/时间戳)显式化 | 随①一并落地 |
105
+ | 3 | ② GC 按 retention 排序 + lastAccessAt 访问强化 | 之后 |
106
+ | 4 | ③ supersedes 门控规格(默认关)| 场景积累后评估 |
107
+
108
+ > 决策依据:不追榜单分数(coding 赛道区分度不足);以自建 bench(PersonaMem/真实库)为增强度量尺。
@@ -0,0 +1,81 @@
1
+ # dsh-engram 改进方案 · 借自 Beads(依赖图 / 可逆压缩 / prime 注入)
2
+
3
+ > 依据:[gastownhall/beads](https://github.com/gastownhall/beads) 深度研究(Go + Dolt 的分布式图式 issue tracker)。
4
+ > 约束:全部**纯规则 / 零向量 / 零 LLM 蒸馏 / 确定性单测 / host 等自然重启**。
5
+ > 现状:ESR 任务有 draft→active→stable + 证据门,但**无任务依赖 / 无认领所有权 / 无会话开始注入**。
6
+
7
+ ---
8
+
9
+ ## 总览
10
+
11
+ | # | 概念(来源) | 解决什么 | 落点 | 成本 |
12
+ |---|---|---|---|---|
13
+ | ① | **依赖图 + ready 队列 + 原子 claim**(blockedstate.go / claimer.go / ready) | 任务各自为政:没有「谁挡谁」、没有可认领队列、没有所有权 | TaskSchema deps/assignee + `esr_dep`/`esr_claim`/`esr_unclaim`/`esr_ready` + board 阻塞标记 | host ~350 行 |
14
+ | ② | **可逆压缩**(compactor.go SnapshotIssue 先归档再覆盖) | 闭环任务全文占看板/上下文;丢了细节 | esr_close 自动 `snapshot` + 规则 `summary`;board 显示摘要、可回看原文 | host ~60 行 + board |
15
+ | ③ | **prime 注入模式**(prime.go 预算分层) | esr_model 只能按需查全文,无法 50-token 会话注入 | `esr_model` 加 `brief/full` + `max_chars` | host ~50 行 |
16
+
17
+ 顺序:**① → ② → ③**(① 是核心增量,②③ 轻量收尾)。
18
+
19
+ **状态:① ✅ ② ✅ ③ ✅**(单 commit 落地,见 git log)。宿主 84/84(beads 12 + compaction 2 + prime 3)、
20
+ dock 63/63、board 59/59(含 4 条 beads UI)。host 侧(esr_dep/esr_claim/esr_unclaim/esr_ready、compactOnClose、
21
+ esr_model mode/max_chars)随下次 `dsh web` 重启生效;board 三处显示(🔒 blocker / @assignee / 🗜 摘要)
22
+ client-hmr 即时生效。
23
+
24
+ ---
25
+
26
+ ## ① 依赖图 + ready + claim
27
+
28
+ ### 数据模型(TaskSchema 增量,向后兼容)
29
+ ```jsonc
30
+ // 既有字段不动;新增:
31
+ "deps": [{ "id": "tsk_2", "kind": "blocks" | "relates-to" | "parent-of" }],
32
+ "assignee": null, // claim 者
33
+ "claimedAt": null
34
+ ```
35
+ 旧任务无这些字段 → schema default 兜底,不动存量。
36
+
37
+ ### 语义(借鉴 BlockedStateInvariant,纯派生)
38
+ - `isBlocked(ws, id)`:存在一条**未关闭**(非 stable/未 archive)的 `blocks` 边指向自己 → blocked;或被 `parent-of` 链上父级 blocked 继承 → blocked。
39
+ - `readyTasks(ws)`:active/draft 中 `!isBlocked` → 可认领队列。
40
+ - `claimTask(ws, id, agent, {force})`:已被他人 claim(assignee 非空且 ≠ agent)且非 force → 拒绝(anti-yank fence);成功置 assignee+claimedAt+state=active。
41
+ - `unclaimTask(ws, id, agent, {force})`:同上 fence 反向。
42
+ - 校验:`addDep` 防自引用 + **有向环检测**(BFS 从 dep 出发不可达自身即可)。
43
+ - close 解锁:blocked 是派生的,无需显式释放(读时计算);cycle 检测写时校验。
44
+
45
+ ### 工具(host)
46
+ - `esr_dep {task_id, dep_id, kind}` → addDep,返回当前 dep 列表
47
+ - `esr_claim {task_id, agent?, force?}` / `esr_unclaim {task_id, force?}`
48
+ - `esr_ready {ws?, limit?}` → 无 blocker 的 active/draft 列表(一行一条 + 缺口)
49
+ - 全部走既有 parse/guard/中文返回;拆写不入 dock。
50
+
51
+ ### Web
52
+ - board 卡上:被 blocker 阻塞 → 卡头 🔒 + title 提示「被 N 个未闭环任务阻塞」;有 assignee → 卡 meta 显示 `@assignee`。(不做编辑,编辑走工具。)
53
+
54
+ ### 测试
55
+ `test/beads.test.mjs`(或并入现有):addDep 自引用拒 / 环拒 / isBlocked 直连+继承 / ready 过滤 / claim fence / unclaim fence / close 后 blocker 解锁 / route 注册。
56
+
57
+ ---
58
+
59
+ ## ② 可逆压缩(闭环快照)
60
+
61
+ - esr_close 进入 stable 时:若 `description.length > 240` 字符 → `summary = 首行/前 120 字`(规则截断,无 LLM),`snapshot = { description, artifact, evaluation, memoryRefs }` 原文入库存档;否则 summary=null。
62
+ - board 卡:`summary` 存在则显示摘要并标注「已压缩 · 原文见详情」;展开区回看全文(snapshot)。
63
+ - 语义:上下文只看摘要、信息可逆,与 compact.go 的「先归档再覆盖」同序(我们是先归档再展示)。
64
+ - 新增 `esr_task --full`(或 esr_detail 变体)回看 snapshot?最小:board 展开即可,不加新工具。
65
+
66
+ ---
67
+
68
+ ## ③ prime 注入模式(预算分层)
69
+
70
+ - `esr_model` 加参数 `mode: "brief"|"full"`(默认 full)、`max_chars`。
71
+ - `lib/mental.js` 加 `compileBriefSummary(domain, ws)`:只输出核心一行(任务 X 进行中/Y 就绪 · 实体 a b · 观测 N 条 · 风险 0)——对应 beads 的 ~50-token MCP 提醒。
72
+ - brief 同样吃 dirty/水印缓存(复用 getModel,仅渲染模式不同)。
73
+ - 工具文档更新:建议会话开始用 `esr_model --brief` 注入;全量按需用 full。
74
+
75
+ ---
76
+
77
+ ## 风险与不做
78
+ - 不做 Dolt/分布式 sync / wisp 消息平面 / AI Haiku 压缩 / contributor-maintainer 路由。
79
+ - 不做 board 依赖编辑 UI(工具优先,dock 保持极简)。
80
+ - cycle 检测 O(V+E) 每写一次,任务数小(≤40/ws)无压力。
81
+ - addDep 对 stable/archived 目标仍放行(可引用历史),仅校验存在性。
@@ -0,0 +1,188 @@
1
+ # dsh-engram 改进方案 · Context GC(自动 GC = 替代自动 compact,而非记忆面板回收)
2
+
3
+ > 依据:[pi-esr Context GC](https://github.com/skepsun/pi-esr/blob/master/extensions/CONTEXT_GC.md)
4
+ > (`extensions/context-gc.ts` + `CONTEXT_GC.md`,v0.7.0 落地)deep-dive。
5
+ > 一句话定位(用户口径):**pi-esr 的自动 GC 用于替代自动 compact(如 DSH 自带的上下文压缩),
6
+ > 而不仅仅是记忆面板里的 gc**。
7
+ > 约束:全部**纯规则 / 确定性 / 可回退 / 默认不烧额外 LLM(可配置)**;host 随 `dsh web` 重启生效。
8
+ > **状态:提案待审(未实施)。**
9
+
10
+ ---
11
+
12
+ ## 1. 现状与缺口
13
+
14
+ | 面 | 现状 | 缺口 |
15
+ |---|---|---|
16
+ | 记忆面板 GC | `esr_gc` 工具 + 定时 `runGcAll()` → `store.gc()`:TTL 过期归档、超容量淘汰、stable 任务 retention、悬空链接清理(只归档、不硬删) | 这**只是存储维护**,完全不参与上下文窗口 |
17
+ | DSH 自动 compact | 基座 bundle 挂 `@deepseek-ai/dsh-compaction-basic`(`BasicCompactionEngine`):pressure / context-overflow 时用 `ctx.llm.stream()` 做**有损全量 LLM 摘要**,替换旧轮次 | 有损、不可查询、摘要本身烧上下文;与 engram 已持久化的可重取细节重复付费 |
18
+
19
+ > 结论:dsh-engram 目前**没有**接入 DSH 的 compaction 接缝,所以「自动 GC = 替代自动 compact」这一层完全缺失。
20
+ > 本提案只补这一层;记忆面板 GC 保留原样(两者职责正交,互不干扰)。
21
+
22
+ ---
23
+
24
+ ## 2. 核心思路(对齐 pi-esr,落到 DSH 接缝)
25
+
26
+ pi-esr 在 pi 侧 hook `session_before_compact`,把「被驱逐消息」扫描出 provenance 锚,返回**指针摘要**
27
+ 替代 LLM 压缩;只有**无锚轮次**才走 scoped LLM 叙事兜底。
28
+
29
+ DSH 侧的等价接缝(已核实源码):
30
+
31
+ - `ctx.compaction` = 抽象的 `CompactionEngine`,默认实现 `BasicCompactionEngine`(`@deepseek-ai/dsh-compaction-basic`)。
32
+ - `BasicCompactionEngine` 的**唯一子类钩子**是 `protected summarize(input, agent, signal)`;
33
+ 其 `SummarizationInput.messages` = **被驱逐的 shadowed region(surface 顺序的完整消息)** ——
34
+ 正好等价于 pi 的 `messagesToSummarize`。
35
+ - `summarize` 返回 `SummaryResult` 后,`compactSurfaceRegion` 会统一经 `frameSummary()` 包装成
36
+ `user/message`(compactCheckpointSource + `<compacted-summary>` framing)落盘 —— 锁、回放校验、
37
+ tool-call/result 配对平衡、token 定价全都不用我们碰。
38
+
39
+ **方案:`ContextGcEngine extends BasicCompactionEngine`,只重写 `summarize()`**,
40
+ 把「LLM 全量摘要」换成「provenance 扫描 → 指针摘要 +(可选)无锚叙事」。
41
+ DSH 的自动 compact 一切触发/机制照旧(pressure / context-overflow / `/compact`),只是**摘要内容换成 GC 产物**。
42
+ 这就是「替代自动 compact」在 DSH 里的最小正确落地。
43
+
44
+ 六条 GC 约束映射(pi-esr `concept-context-gc` → dsh-engram):
45
+
46
+ | pi-esr 约束 | dsh-engram / DSH 行为 |
47
+ |---|---|
48
+ | stable-task-evictable | stable 任务(含 artifact/snapshot,记忆面板已归档原文)→ 指针 `esr_ready` / 看板回看,不逐字复述 |
49
+ | indexed-output-evictable | engram 已落库记忆(`engram_store` / auto-capture / recall 命中的 `#xxxxxx`)→ 指针 `engram_detail(id)` |
50
+ | loom-stored-evictable | 同 engram 记忆 + `filePath` 锚(提示性,不单独作重取调用) |
51
+ | working-set-protected | active 任务 + 其 `memoryRefs` 引用记忆 + 相关 constraint → 在摘要里**复述**,永不驱逐 |
52
+ | pointer-salience | 每个被驱逐类别都带**显式重取调用**(工具名 + 参数) |
53
+ | no-provenance-no-evict | 无锚轮次 → scoped LLM 叙事(默认开,可配 `gcNarrative`);失败/关闭 → 截断原文兜底 |
54
+
55
+ ---
56
+
57
+ ## 3. 实现设计
58
+
59
+ ### 3.1 新文件 `lib/context-gc.js`(host,~380 行)
60
+
61
+ ```
62
+ scanProvenance(messages) # 纯函数:消息 → ProvenanceReport
63
+ groupTurns(messages, anchored) # 纯函数:按 user 消息切轮次,标 anchored
64
+ buildPointerSummary(report, domain, ws) # 纯函数:→ 指针摘要(零 LLM)
65
+ summarizeUnprovenanced(turns, ctx, signal, { narrative }) # 叙事/截断兜底
66
+ ContextGcEngine extends BasicCompactionEngine
67
+ - summarize(input, agent, signal) # 唯一重写点
68
+ - gcConfig (gcNarrative / gcNarrativeMaxTokens / gcNarrativeMaxChars / gcFallback)
69
+ ```
70
+
71
+ **扫描规则**(DSH `Message.content` 的 `tool-call` / `tool-result` block):
72
+ - 锚工具:`engram_store` / `engram_recall` / `engram_detail` / `esr_task` / `esr_close` /
73
+ `esr_ready` / `esr_link` / `esr_dep` / `esr_claim` / `esr_unclaim` / `esr_node` / `esr_model`。
74
+ - 提取:memory id(`#xxxxxx` / `mt…`,工具结果回显 + `engram_detail` 参数)、task/entity id
75
+ (`task-*` / `ent_*`)、`filePath` 存在性。
76
+ - **管理工具不算锚**:`esr_gc`(同 ctx_purge 不重跑原则,绝不指示模型重跑 GC)。
77
+ - 无模型调用、纯字符串/正则,确定性可单测。
78
+
79
+ **指针摘要**(替换默认摘要正文,仍走 `frameSummary` 包装):
80
+ ```
81
+ ## Context GC — 被驱逐细节可重取
82
+ Evicted N turn(s): X with re-fetchable provenance, Y narrative-only.
83
+ ### engram 记忆(可精确重取)
84
+ - `engram_detail(id: "…")` # 被驱逐段内已落库的记忆
85
+ - `engram_recall(query: "…")` # 被驱逐段的检索线索(来源查询)
86
+ ### ESR 任务状态
87
+ - active 工作集复述:`tsk_… [active] · 约束 …`
88
+ - stable(已闭环/已快照)→ `esr_ready` / 看板回看
89
+ ### 未在其他地方保留的叙事
90
+ <narrative 或 "None — all evicted turns had re-fetchable provenance.">
91
+ ---
92
+ _优先重取,不要重新推导。_
93
+ ```
94
+
95
+ **叙事兜底**(`summarizeUnprovenanced`):
96
+ - `gcNarrative: true`(默认)且存在可用路由 → scoped `ctx.llm.stream()` 一次性调用
97
+ (`purpose: 'compaction'`、`maxTokens: gcNarrativeMaxTokens=1024`、复用被驱逐消息前缀),
98
+ 仅此路标记 `llmStreamCall: true`。
99
+ - 无模型 / 失败 / `gcNarrative: false` → 截断原文(`serializeConversation`,上限 4000 字符,标注截断)。
100
+ - 无无锚轮次 → 输出 `_None_`,**完全零模型调用**(最常见路径:编程会话大多有工具锚)。
101
+
102
+ **可回退(永不破坏 compact)**:
103
+ - `summarize()` 内任何扫描/读取/叙事错误 → `return super.summarize(input, agent, signal)`(默认 LLM 摘要)。
104
+ - `gcReplacesCompaction: false` → 引擎根本不加载,DSH 继续走 compaction-basic。
105
+ - engram 域未就绪(无 storage-domain 的 profile)→ 同上回退,不阻塞宿主启动。
106
+
107
+ ### 3.2 `lib/index.js` 增量
108
+
109
+ - `DEFAULTS` 加:
110
+ ```js
111
+ gcReplacesCompaction: true, // 接管 DSH 自动 compact(false=完全回退默认)
112
+ gcNarrative: true, // 无锚轮次走 scoped LLM 叙事;false=截断原文(纯机械)
113
+ gcNarrativeMaxTokens: 1024, // 固化默认即可,不进设置 UI
114
+ ```
115
+ - `ctx.effect` 内(`index.js`):`loadCompactionEngine(ctx, { auto: true }, deps, { mode })` → `new Engine()`,
116
+ 注册 `compaction` 服务;`mode = gcReplacesCompaction ? "context-gc" : "default"`。Service 实例注册即生效、
117
+ 随 fiber 卸载自动移除(cordis `Service` 语义)→ 卸载/reload engram 自动回滚,天然可逆。
118
+ - 记忆面板 GC(`gcEnabled` / `esr_gc`)原样保留。
119
+
120
+ ### 3.3 装配(mounting,实施时已运行时验证 cordis 同名 service 语义)
121
+
122
+ **验证结论**(cordis `reflect.provide` + loader `isolate` 源码 + loader-composition 测试):
123
+ cordis **拒绝**同一隔离域的第二个同名 provider——`provide()` 直接抛
124
+ `service "compaction" has been registered at <fiber>`,**不是**「更深/更晚 wins」。
125
+ 因此装配=下面两条,都已落库:
126
+
127
+ 1. **基座行禁用(必须)**:dsh-engram 的 `cordis.patch.yml` 增加
128
+ ```yaml
129
+ - id: compaction-basic
130
+ disabled: true
131
+ ```
132
+ bundle patch 层按 bundle 顺序应用、dsh-engram 晚于 dsh-base → 该行生效;卸载 engram 即自动还原。
133
+ 禁用后 `compaction` 由 dsh-engram 独占提供:`gcReplacesCompaction:true` → ContextGcEngine,
134
+ `false` → 裸 `BasicCompactionEngine`(与默认行为一致),因此 **`compaction` 服务永不缺席**。
135
+ 2. **preset 面条目(web profile 必需)**:web 面把 compaction 移到 agent preset 的 isolate realm
136
+ (dsh-web-app 禁用基座行、`standard` 预设自挂 `compaction-basic`),host 平面条目进不去该 realm。
137
+ 为此新增可挂载条目 `lib/compaction.js`(`name: dsh-engram-compaction`、`inject: [llm, tokenMeter,
138
+ sessions]`、`package.json` `./compaction` export),把预设 `compaction` 组里的 `compaction-basic`
139
+ 行换成 `- name: dsh-engram/compaction` 即按 session 生效(recipe 见 README)。
140
+
141
+ ### 3.4 依赖与设置
142
+
143
+ - `package.json` peerDependencies 追加(均 optional):
144
+ `@deepseek-ai/dsh-compaction`、`@deepseek-ai/dsh-compaction-basic`(宿主基座已装,此处仅声明类型与导入契约)。
145
+ - `settings.js` schema 追加 `gcReplacesCompaction` / `gcNarrative`;`SETTINGS_KEYS` 同时收录二者。
146
+ 设置 UI 收敛原则(assessment P4)下**只把 `gcNarrative` 放进高级折叠**,`gcReplacesCompaction` 走
147
+ profile / preset 配置。
148
+ - `peerDependencies` 追加(均 optional):`@deepseek-ai/dsh-compaction`、`@deepseek-ai/dsh-compaction-basic`、
149
+ `@deepseek-ai/dsh-llm`;`setup-links.mjs` 补三条 junction(`.mjs` 开发工作流)。
150
+
151
+ ---
152
+
153
+ ## 4. 测试(`test/context-gc.test.mjs`)
154
+
155
+ | 组 | 用例 |
156
+ |---|---|
157
+ | 扫描 | tool-call/tool-result 提取 memory id / task/entity id / filePath;`esr_gc` 排除;tool-result 回显关联 |
158
+ | 轮次 | 按 user 切轮、anchored 透传、无锚轮次归集 |
159
+ | 指针 | 合成 report + 假 domain:working-set 复述、stable 指针、`_None_` 叙事、footer |
160
+ | 叙事 | 关(纯机械、无 llmStreamCall)/ 开且成功(llmStreamCall)/ 失败截断兜底 |
161
+ | 引擎 | 假 ctx + 假 session:`summarize()` 产指针块不落错;**错误 → 回退 super**(注入可替换的 defaultSummarize 以便单测);`gcReplacesCompaction:false` 不加载 |
162
+ | 装配 | config 合并键存在;`loadCompactionEngine` 两 mode 各返回正确类;preset 条目 shape + apply 挂载(default/context-gc 各断言受托引擎) |
163
+
164
+ 回归:`npm test`(当前 143/143 全绿)+ `npm run build:client`。
165
+
166
+ ---
167
+
168
+ ## 5. 风险与不做
169
+
170
+ - **不做主动触发**(等价 pi `--gc-auto` / `--gc-auto-percent`):只跟随 DSH 现有时机(pressure /
171
+ context-overflow `/compact`)——用户已确认,改动最小。
172
+ - **不重写** retention walk / token 定价 / 锁 / 回放校验 / tool 配对平衡:全部复用 `BasicCompactionEngine`。
173
+ - **不与 tool-result pruner 冲突**:`ctx.toolResultPruner` 在 range 选择前先压大 `tool/result` 节点,
174
+ 我们的 `summarize` 收的是 prune 后的 shadowed region;指针指向 engram 本体记忆(不受 prune 影响)。
175
+ - 指针摘要比默认摘要短 → 每次 compact 省 token 可量化(落盘前用 `ctx.tokenMeter.estimateMessage` 记一次)。
176
+ - 极端场景:无任何锚且 `gcNarrative` 关 → 截断原文可能比默认 LLM 摘要长;有 `gcNarrativeMaxChars` 上限兜底,
177
+ 且这是用户显式关闭叙事的取舍。
178
+
179
+ ---
180
+
181
+ ## 6. 落地顺序
182
+
183
+ 1. `lib/context-gc.js` 纯函数层(scan / group / pointer / narrative-fallback)+ 单测(先行,无依赖)。
184
+ 2. `ContextGcEngine` + `summarize()` 重写 + 回退注入 + 单测。
185
+ 3. `index.js` 装配 + config 键 + 装配单测(含「卸载回退」验证)。
186
+ 4. 设置 schema / UI 高级折叠(`gcNarrative`)。
187
+ 5. README.zh.md「记忆 GC」章节扩为「自动 GC = Context GC(替换自动 compact)+ 记忆面板 GC」,FEATURES.zh.md 记一笔。
188
+ 6. 全量 `npm test` 回归 + `npm run build:client`(若 UI 有改动)。
@@ -0,0 +1,199 @@
1
+ # 提案:ESR 触发机制对齐 pi-esr —— 静态方法论 + 会话快照 + 拉取式触发
2
+
3
+ - 状态:**已实施**(含范围「完整实施」+「提示行挪到 esr_status」两项批准决策)
4
+ - 面向:dsh-engram `[ESR]` 块的触发/提示机制(P0-A/B + P2 + P3 + P4)
5
+ - 参考:pi-esr(`@pi-esr/…`)——README「为 LLM 前缀缓存稳定性而生,每字节确定」(三个不变式 + 状态机 + 增量读取)
6
+
7
+ ---
8
+
9
+ ## 1. 为什么现在改(当前机制的不稳定点)
10
+
11
+ `[ESR]` 块是 dsh-engram 模型侧触发 ESR 的全部入口。当前实现(`lib/index-block.js` `renderEsr` +
12
+ `lib/trigger.js` `makeTriggerRecorder` + `lib/index.js` systemPrompt section 装配):
13
+
14
+ 1. **`[ESR]` 块每轮系统提示词装配都重新渲染**。对比 `[ENGRAM]` 走 `blockFor()` 的每会话
15
+ `sessionCache` 冻结缓存,`engram:esr` section 的 `text()` 直接调 `renderEsr(...)`——每次都带着
16
+ live recorder 重跑。于是逐轮注入内容漂移 → **打断前缀缓存(KV cache)**,会话越长成本越高。
17
+ 2. **块内混入推式启发式提示行**:`promote:` / `root-cause:` / `close:` / `stale:` / `escalate:`
18
+ 随运行态出现/消失(一旦强制一次/会话抑制、magic 阈值 `minTodosForPromote`/`todoPromoteRatio`/
19
+ `minErrorHits`/`staleTaskDays`/`escalate 0.34`)。`sortActiveForNext` 的末位 tiebreak 用
20
+ `updatedAt` → 同状态任务排序随时间漂移。这些行**每轮都可能不同**,无法复现、难以测试,也不稳定。
21
+ 3. **教模型「何时用 ESR」靠运行时 nudge,而非静态方法论**:协议(建任务→升 active→证据闭环)
22
+ 依赖提示行出现才被记得;提示被 once 抑制后协议也就不可见了。
23
+ 4. **推式状态全在进程内内存**(todo 快照、mem/esr 计数、once 抑制集合)→ 宿主重启即清、跨会话
24
+ 不可见、纯时序依赖。
25
+
26
+ > 一句话:**触发信号从「每轮推送进前缀」变成「每轮推送进前缀」的错误姿势**——正确姿势是
27
+ > 「前缀里只有字节恒定的方法论+快照,实时状态按需拉取」。
28
+
29
+ ---
30
+
31
+ ## 2. pi-esr 参考(「稳定」的三个不变式 + 状态机)
32
+
33
+ pi-esr README.zh-CN:*「一个受约束的语义图状态机,专为……设计。为 LLM 前缀缓存稳定性而生——
34
+ 每个字节都是确定性的。」*
35
+
36
+ | # | 不变式 | pi-esr 落地 | 我们当前的违背点 |
37
+ |---|---|---|---|
38
+ | 1 | **系统提示词运行时永不变化** | `prompts/esr.md` 静态方法论 + Golden Rules + Minimum workflow,每轮逐字节相同 | `[ESR]` 块每轮重渲染、提示行增删 |
39
+ | 2 | **会话快照注入一次、绝不中途自动刷新** | `[ESR_CONTEXT]` 在 SessionStart 注入,末尾明示 "This snapshot WILL NOT auto-refresh. Call `esr_get_context`…";上下文**确定性排序**(entity by id、relation by (from,type,to)),**时间戳排除在上下文外**(保字节稳定) | `[ESR]` 从未冻结;`sortActiveForNext` 用 `updatedAt` 排序 |
40
+ | 3 | **增量读取** | `esr_get_context(since_revision=N)`:版本一致 → ~10 token 的 "ESR state unchanged since revision N";不一致 → 全量确定性快照 | 没有拉取工具;状态只在块里 |
41
+ | 4 | **状态机** | draft → active → stable(blocked/deprecated);每次转换自动 journal;`esr_complete_task` 一站式证据闭环;「State is the single source of truth」 | 已有 draft→active→stable + 证据门(对齐),无需大改 |
42
+
43
+ 另外 pi-esr 把「何时该创建/提升/闭环」写进**协议文本**(`When to call ESR` / `Minimum workflow` /
44
+ `Golden rules`),而不是运行时 nudge——稳定且提示串可复用。
45
+
46
+ ---
47
+
48
+ ## 3. 推荐设计(dsh-engram 落地:静态 + 快照 + 拉取)
49
+
50
+ ### 3.1 静态方法论 section(新增 `engram:esr-method`,字节恒定)
51
+
52
+ 固定文本(仿 pi-esr Golden Rules / Minimum workflow,改成 dsh-engram 工具名),每轮逐字节相同:
53
+
54
+ ```
55
+ ESR(工程状态)操作协议 —— 多步工作建任务、用证据闭环:
56
+ 1. 动工前:esr_ready 看可认领工作;esr_status 拿实时状态(本块是会话开始快照,不会自动刷新)。
57
+ 2. 多步工作 → esr_task(name="…", entity=…) 建任务(draft 起步);重复对象 → esr_node;关系 → esr_link。
58
+ 3. 动工 → esr_claim(draft → active)。
59
+ 4. 收工 → esr_close(artifact + evaluation + memory_ref 三证齐才算数;不齐的活继续挂着,别硬关)。
60
+ state 是唯一真相:拿不准 state 就 call esr_status。
61
+ ```
62
+
63
+ 装配在 `esrOrder` 之前(如 order-1),**不依赖任何运行态**。DSH systemPrompt section 本身就
64
+ 按 section 缓存;该 section 返回的文本只由静态内容组成 → 字节稳定。
65
+
66
+ ### 3.2 `[ESR]` 块改为「每会话冻结快照」(复用 `blockFor` 的 sessionCache)
67
+
68
+ - `renderEsr` 放进与 `[ENGRAM]` 相同的 `sessionCache` WeakMap(新 session → 渲染一次)。
69
+ - 快照内容(全部**确定性派生**,无 LLM、无 recorder、无时间戳):
70
+ - `[ESR] tasks: N active / M stable / K draft`
71
+ - 证据齐的最优先行:`next: esr_close <id>`(`evidenceGaps` 纯派生)——保留
72
+ - 活动任务列表按 `READY→claimed→active→draft` 排序列出;**最后 tiebreak 从 `updatedAt`
73
+ 改为 `id` 字典序**(等价 pi-esr「时间戳排除在上下文外」;同一 store 快照 → 字节相等)
74
+ - `closed:` 行(stable 汇总)——保留
75
+ - 固定尾行:`snapshot from this session's start — WILL NOT auto-refresh; call esr_status for live state`
76
+ - **删除**全部 `promote:/root-cause:/close:/stale:/escalate:` 行与 `#suggest-*` 来源标记(从块内)
77
+ - 空态时保留一行固定的「BE PROACTIVE」教模型建任务(静态文本,非 nudge)
78
+ - 无 session 的渲染(GUI 预览)天然不带 hints,行为不变。
79
+
80
+ ### 3.3 `esr_status` 拉取工具(新增,替代推式提示的全部「实时」职责)
81
+
82
+ 工具(读类,zero-写):`esr_status(workspace?, since_revision?)`
83
+
84
+ - **全量视图**:确定性排序的活动/稳定/draft 任务 + `next:` 行 + stable 汇总(≈现 `renderEsr`
85
+ 的静态部分,但含活数据)。
86
+ - **增量**:进程内维护 workspace 级 `revision`(每次任务写操作 +1,或像 pi-esr 那样用
87
+ DJB2 指纹 `buildGraphFingerprint`,二选一,实施时定);`since_revision ≥ 当前` →
88
+ 返回 `ESR state unchanged since revision N (~5 行小响应)`;不一致 → 全量。
89
+ - **派生 actionables(原 P0-A/P0-B/P2/P3 转变而来,只在「拉」时产生)**:
90
+ - `promote:` — 读 recorder 的 todo 快照(`minTodosForPromote`/`todoPromoteRatio` 保留,但只在此 apply)
91
+ - `root-cause:` — 读 store 的复发错误候选(`minErrorHits` 保留)
92
+ - `close:` / `stale:` — 读 `workDone` 标记 / `staleTaskDays` 计算(stale 按 task update 时间
93
+ 在**拉取时**算——不进前缀,时间敏感无碍)
94
+ - `escalate:` — 读 recorder 的 mem/esr 实时计数
95
+ - 每条仍带 `#suggest-*` 标记(可引用、可归因)
96
+ - 由于这些行只在按需返回里出现,**永不进入每轮前缀** → 前缀稳定性不受影响。
97
+
98
+ ### 3.4 `[ENGRAM]` index 块保持现状(已经冻结,不在此提案范围)
99
+
100
+ ### 3.5 P4 转化度量适配
101
+
102
+ - `emitHint(kind, ws, sessionId)` 从「`renderEsr` 注入块时调用」改为「`esr_status` 返回该 hint
103
+ 时调用」(真实返回路径);`tools/result` 订阅里 esr_* 的 10 分钟窗口归因**保持不变**。
104
+ - `/triggerstats` API/口径文案微调(触发源:`esr_status` 返回,而非块注入),路由不变。
105
+
106
+ ### 3.6 保留 / 移除对照
107
+
108
+ | 现状 | 动作 |
109
+ |---|---|
110
+ | `[ENGRAM]` 冻结块 | 保留 |
111
+ | 记忆面板 GC、Context GC | 保留(刚实施) |
112
+ | `esr_task/esr_close/esr_link/esr_dep/esr_ready/esr_node/esr_claim` 工具与证据门 | 保留 |
113
+ | GUI「沉淀到 ESR」按钮 | 保留 |
114
+ | `tools/result` 订阅喂 recorder(todo 快照、mem/esr 计数) | **保留**(转成 esr_status 的拉取数据源;不再喂块渲染) |
115
+ | `[ESR]` 块逐轮重渲染 | **移除** → 每会话冻结快照 |
116
+ | 块内 `promote/root-cause/close/stale/escalate` 行 | **移除** → 移到 `esr_status` 拉取结果 |
117
+ | `sortActiveForNext` 的 `updatedAt` tiebreak | 改为 `id` 字典序(确定性) |
118
+ | 静态方法论 | 新增 `engram:esr-method` |
119
+ | `esr_status` | 新增(含增量 revision) |
120
+ | 配置键 `minTodosForPromote/todoPromoteRatio/minErrorHits/staleTaskDays` | 保留(仅作用于 esr_status 派生行) |
121
+
122
+ ---
123
+
124
+ ## 4. 测试(`test/` 新增/改动)
125
+
126
+ | 组 | 用例 |
127
+ |---|---|
128
+ | 快照确定性 | 同一 store 快照下 `renderEsr` 两次调用输出**逐字节相等**;输出**不含** `#suggest-*`/`promote:`/`escalate:` 等 hint 行 |
129
+ | 排序确定性 | `sortActiveForNext` 对同 rank 任务按 id 稳定排序(不再用 updatedAt 漂移) |
130
+ | 版本/增量 | `revision` 单调递增(写任务 +1);`esr_status(since_revision=当前)` → short "unchanged" 响应;过期 rev → 全量 |
131
+ | 派生行 | esr_status 含 promote(todo 快照条件)/ root-cause(hits≥阈值)/ close(workDone)/ stale(超窗)/ escalate(mem ratio),每条带 `#suggest-*` |
132
+ | 静态方法论 | `engram:esr-method` 文本与运行态无关(两轮装配字节相等) |
133
+ | 回归 | 现有 143 测试全绿(`renderEsr` 的既有调用方/测试按新语义适配:无 recorder 时=纯快照) |
134
+
135
+ ---
136
+
137
+ ## 5. 风险与不做
138
+
139
+ - **不做**:不删 `tools/result` 的 recorder 订阅(它是 esr_status 派生行的数据源);
140
+ 不做 pi-esr 式「协议硬门」(PreToolUse 强拦)——DSH 是自由 agent,硬门会进交互层,暂不做;
141
+ 不做 `since_revision` 的持久化(进程内 revision 足够;跨宿主重启全量返回,代价可接受)。
142
+ - **行为变化**:`[ESR]` 块不再逐轮提醒 → 依赖提示的旧行为消失,由静态方法论 + 用户/模型主动
143
+ `esr_status` 替代。这是「稳定换主动」的取舍,正是 pi-esr 的取向。
144
+ - **GUI 预览**:`renderEsr` 不带 recorder 时本来就渲染静态视图,改动后预览与真实快照一致。
145
+ - `updatedAt` 不进入快照(连 tiebreak 都不依赖它)→ stale 只存在于拉取视图,天然与缓存解耦。
146
+
147
+ ---
148
+
149
+ ## 6. 落地顺序
150
+
151
+ 1. `index-block.js`:`sortActiveForNext` tiebreak 改 id;`renderEsr` 去掉 hint 分支(保留
152
+ `next:`/列表/closed/静态 BE PROACTIVE/尾行);配套单测。
153
+ 2. `index.js`:`[ESR]` 并入 `sessionCache` 冻结渲染;新增 `engram:esr-method` 静态 section。
154
+ 3. `store.js`:workspace 级 `revision` 计数(写路径 +1)+ `buildWorkspaceFingerprint`(可选)。
155
+ 4. `tools.js`:新增 `esr_status`(全量 / since_revision 增量 / 派生 actionables)。
156
+ 5. `trigger.js`:`emitHint` 改从 esr_status 返回路径调用;其余逻辑保留为拉取数据源。
157
+ 6. 文档:README.zh/README.md 的「决策点触发」章节改写为「静态协议 + 会话快照 + 拉取触发」;
158
+ FEATURES.zh.md 记一笔。
159
+ 7. 回归 `npm test` + `npm run build:client`。
160
+
161
+ ---
162
+
163
+ ## 7. 验收
164
+
165
+ - `[ESR]` 块在同一会话内两次装配**逐字节相同**(前缀缓存稳定)。
166
+ - 同一 store 快照下 `renderEsr` 输出确定性(无时间戳、无 recorder、无 `#suggest-*`)。
167
+ - `esr_status` 提供增量短响应 + 全量确定性视图 + 全部派生 actionables,各自带稳定来源标记。
168
+ - 现有 143 测试全绿 + 新增测试通过;客户端构建通过。
169
+
170
+ ---
171
+
172
+ ## 8. 实施记录(与提案的差异)
173
+
174
+ - **revision 采用任务表面指纹,而非单调计数器**:`store.esrFingerprint(workspace)` 对
175
+ id/state/evidence/认领人排序后 sha256 前缀(`hashText`)——零持久化、跨重启一致、时间戳天然排除
176
+ (提案 §4「决定在实施时定」:计数器 vs 指纹 → 选指纹;持久化 → 不持久化全量兜底)。
177
+ - **`[ESR]` 头部把 draft 单列**:`tasks: N active / M stable / K draft`(N 不含 draft,避免歧义)。
178
+ - **`esr_status` 响应统一走 `buildEsrStatusView`**(index-block.js 导出):始终重算 actionables
179
+ (易变部分——balance/stale 窗口),`since_revision` 只短路**状态视图**;actionables 在
180
+ unchanged 分支仍会返回。
181
+ - **gui/api 的 `renderEsrBlock` 变为纯快照**(hint-free),与冻结前缀一致;预览即真值。
182
+ - **测试**:143 → 148(新增:指纹时间戳排除、buildEsrStatusView 全量/增量/过期、方法论字节恒定、
183
+ 快照 hint-free 确定性);`trigger.test.mjs` 全部提示测试改走 `esrHintLines` / `buildEsrStatusView`。
184
+
185
+ ### 8.5 再修订:混合方案(提升行为主动性,用户二轮拍板)
186
+
187
+ - 用户反馈:纯拉取让「主动建任务/实体/关系」不保证(模型不调 `esr_status` 就看不到提示)。
188
+ 批准混合:`[ESR]` 块 = **冻结快照 + 本会话单调追加的 actionables**。
189
+ - **实现是「单调渐进」而非字面「会话开始冻结一次」**:会话第一帧装配时 recorder 还是空的,
190
+ 字面冻结等于没提示。正确做法是每条 actionable 在它成熟的那一刻(如模型写完 todo 计划后的
191
+ 下一轮)被 `esrHintLines` 一次性产出 → 追加进块并永久冻结(`holder.actionables` 按 kind 存储、
192
+ `ESR_HINT_ORDER` 固定排序),永不回撤 → 前缀只在「真正出现新决策点信息」时变化一次。
193
+ - 若干细节:`buildFrozenEsrBlock(store, ws, config, {recorder, sessionId, holder})` 是宿主装配入口;
194
+ `[ESR]` 不再用 WeakMap 缓存(渲染是纯确定性派生,变化只来自新 actionables);
195
+ 快照本体保持 hint-free(GUI 预览 `renderEsrBlock` 仍是纯快照);
196
+ `esr_status` 保持纯拉取(实时状态 + 会话中后期才成熟的派生)。
197
+ - 测试:新增「快照纯净 + actionables 单次追加」「单调增长 / 已追加不回撤」、「无 recorder 状态时纯快照」。
198
+ - 文档:README.zh/README.md/FEATURES.zh.md 的 0a 章节改为混合语义,注入块示例含
199
+ `# this-session actionables (frozen)`。