dsh-engram 0.3.4 → 0.3.6

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,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)`。
@@ -0,0 +1,156 @@
1
+ # dsh-engram 改进方案 · 借自 Hindsight(三个概念,不引重路线)
2
+
3
+ > 依据:[vectorize-io/hindsight](https://github.com/vectorize-io/hindsight) 深度研究(2026 春季快照)。
4
+ > 原则:三条全部**纯规则 / 无向量 / 无 LLM 蒸馏 / 确定性可单测**,符合既有约束
5
+ > (不引重路线、中文、host 改动等自然重启、dock 极简)。
6
+ > 真实基线:Kototoro memories 71(扁平)/ tasks 1 / links 4 / nodes 3;toolstats 近 14 天
7
+ > todo 48 · ESR ~6 · esr_node/esr_link 接近零。
8
+
9
+ ---
10
+
11
+ ## 总览
12
+
13
+ | # | 概念(来源) | 解决什么 | 落点 | 成本 | 依赖 |
14
+ |---|---|---|---|---|---|
15
+ | A | **Observation 范式**:证据引用 + proof_count + refine-not-overwrite(consolidator.py) | 71 条扁平记忆无从浓缩;失败/成功经验被覆盖不保留证据链 | `lib/store.js` 写入侧 + `lib/obs.js` 新模块 + board 观测区 | host ~250 行 | 无 |
16
+ | B | **Mental model 预计算**:常驻答案读零计算 + 水印刷新(mental_models + last_memory_seen_at) | 每次会话现查照样费 tokens;dock/board 每次重算 | `lib/mental.js` + 新工具 `esr_model` + board 常驻条 | host ~200 行 | 依赖 A 提供的 observation 汇总 |
17
+ | C | **召回重排信号**:proof_count boost × recency 半衰期(reranking.py) | 现行排序只有 recency+精确匹配;失败复活性差 | `lib/rerank.js` 纯函数 + bm25Rank 接入 | host ~80 行 | 无 |
18
+
19
+ 状态:C ✅(7093fb5)/ A ✅(7103a22)/ B ✅。建议实施顺序:**C → A → B**。
20
+ B 落地要点:`models` 表注册(ws/content/generated_at/dirty/sources_hash);
21
+ `lib/mental.js` 纯函数 compileSummary(任务/实体/观测/记忆种类聚合 → markdown)、
22
+ computeSourcesHash、modelStale(dirty ∨ 超 10min ∨ 输入 hash 变)、getModel(冷缓存重算并持久化);
23
+ esr_* 六个写工具成功路径统一 `markModelDirty`;agent 工具 `esr_model`(返回常驻摘要+生成分钟数)
24
+ 与 `/model` 端点;board 顶部可折叠「常驻摘要 · 生成于 N 分钟前」。新测试 6 个(宿主 67/67)。
25
+ C/A/B 的 host 部分(rerank、/observations、/model、esr_model、dirty 钩子)随下次 `dsh web` 重启生效;
26
+ board 观测条/常驻条 client-hmr 即时生效。
27
+ A 落地要点(相对本稿):`observations` 表注册进 `engramDomainSpec`;
28
+ `storeMemory` 三路(新建/exact-dup/失败复活)均触发 `integrateObservation`,其中
29
+ **失败复活(revive)是「新发生」→ forceEvidence 爬 proof**,exact-dup(同源)只刷
30
+ span;`engram_recall` 尾部注入 proof≥2 的观测段;board 加可折叠「观测 · 信念 N」条;
31
+ `/observations` 端点与 /toolstats 同批等重启;新测试 12 个(宿主 61/61)。
32
+
33
+ ---
34
+
35
+ ## A. Observation:证据浓缩 + 证据计数 + 增强而非覆盖
36
+
37
+ ### 问题
38
+ - 记忆是扁平列表;同一主题的多次失败/多次成功没有聚合,`esr_recall` 返回的是散点。
39
+ - D1/D3 的「失败记忆复活」只恢复了单条,没有形成「这个模式被证明 N 次」的信念。
40
+
41
+ ### 数据模型(新增 `tables.observations`,存 storages/dsh_engram.json)
42
+ ```jsonc
43
+ {
44
+ "id": "obs_<8>",
45
+ "ws": "/ws/A", // "" = 全局
46
+ "text": "浓缩后的信念语句(初始 = 触发记忆文本)",
47
+ "kind": "belief", // belief | pattern | risk(由触发记忆 tags 推导,不引 LLM)
48
+ "proof": { "count": 3, "sources": ["mem_<8>", ...] },
49
+ "span": { "first_seen_at": 1, "last_seen_at": 2 },
50
+ "negations": 0, // 反证计数 → 触发 weaken/stale
51
+ "trend": "stable", // new|strengthening|stable|weakening|stale(算法计算)
52
+ "tags": ["fail"],
53
+ "updated_at": 3
54
+ }
55
+ ```
56
+
57
+ ### 算法(`lib/obs.js`,全部纯函数 + 一个写入钩子)
58
+ 1. **`concentrate(ws, memText, memId, tags)`** —— host 写入记忆(esr_store_memory / 失败复活 / engram 自动捕获)后调用:
59
+ - 桶化:按 `(entity 锚点 或 tags) + 字符 n-gram Jaccard ≥ 阈值(0.45)` 找既有 observation(中文无需向量)。
60
+ - 命中 → **merge 而非覆盖**:`proof.count+1`、`sources.push(memId)`、`span.last_seen_at = now`、`updated_at=now`;`text` 仅在新增细节时 append(简单规则:新文本含旧文本不存在的 token 段则并句)。
61
+ - 命中且可判定为**反证**(tags 含 `error`+当前 text 与旧 text 出现否定词对)→ `negations+1`,`proof.count` 不变。
62
+ - 未命中 → 新建 observation(`proof.count=1`)。
63
+ 2. **`computeTrend(span, now, recentDays=30, oldDays=90)`** —— 借鉴 observations.py 的 `Trend`:
64
+ - last_seen 在 30 天内且 first_seen 更早 → `strengthening`
65
+ - 全部证据在 30 天内 → `new`
66
+ - 90 天内有证据 → `stable`
67
+ - 90 天内无新证据但有历史 → `weakening`;无证据 → 不存在的桶自清(GC 归入 esr_gc)。
68
+ 3. **读取注入**:`esr_recall` 把 `proof.count ≥ 2` 的 observations 作为高权重候选混入返回(标记 `source: observation`),证据门语义不变。
69
+
70
+ ### Web 展示
71
+ - 只进 **board**(dock 保持极简):columns 上方一行可折叠「观测 · N 条信念」条,展示 `text · ✓×count · 趋势图标`,展开可看 sources 列表。
72
+ - KPI 行(toolstats 那行)旁加 `观测 N`。
73
+
74
+ ### 测试
75
+ `test/obs.test.mjs`(node:test 直接 import):
76
+ - 首次写入建桶 / 二次相同语义 merge(count 2 且 sources 追加)/ 反证 negations+1 /
77
+ trend 四种时间窗 / 阈值下不误并 / Jaccard 中文样例。
78
+
79
+ ---
80
+
81
+ ## B. Mental model:常驻摘要 + 水印刷新
82
+
83
+ ### 问题
84
+ - agent 每轮现查「这个工作区现在什么状态」要花多次 recall;看板每次挂载重算 overview + tasks + links。
85
+ - 产出一个可被反复读取、只在数据变化时才重算的「常驻答案」→ 读零计算。
86
+
87
+ ### 数据模型(新增 `tables.models`)
88
+ ```jsonc
89
+ {
90
+ "ws": "/ws/A",
91
+ "content": "## /ws/A 常驻摘要\n- 任务:2 active(1 就绪)/ 1 stable\n- 实体 3 · 链接 4\n- 观测 6 条信念(2 失败模式已重复 3 次)\n- 近 14 天:todo 48 · ESR 6\n- 生成于 N 分钟前",
92
+ "generated_at": 1,
93
+ "dirty": true, // 任何 esr_* 写操作后置 true
94
+ "sources_hash": "sha256 摘要(惰性校验可跳过重算)"
95
+ }
96
+ ```
97
+
98
+ ### 算法(`lib/mental.js`)
99
+ - **`bumpDirty(ws)`** —— host 工具写钩子(esr_task/esr_close/esr_node/esr_link/esr_store_memory/todo 沉淀)之后调用。
100
+ - **`getModel(ws)`** —— 读缓存;`dirty` 或过期(>10min)则**纯聚合重算**(无 LLM):复用 `domain` API 的 tasks/entities/links 计数 + `collectToolCounts` 的近况 + `tables.observations` 的 top 信念 → 拼 markdown;重算后 `dirty=false`。
101
+ - **暴露**:
102
+ - agent 工具 `esr_model(ws?)` —— 返回常驻摘要 markdown(**零 LLM、一次 DB 读**)放在工具说明顶部「先看 model 再 recall」;response 带 `modeling:` 行同款风格。
103
+ - web endpoint `/model?ws=`(route 注册随下次重启,与 /toolstats 同批)。
104
+
105
+ ### Web 展示
106
+ - board 顶部「常驻摘要 · 生成于 N 分钟前」可折叠条(可复制、可下载为 md——复用自己的 buildTasksMarkdown 思路,纯 client 无依赖)。
107
+
108
+ ### 测试
109
+ `test/mental.test.mjs`:dirty→重算 / 未 dirty→命中缓存(generated_at 不变)/ 写操作触发 bump(route 注入假 service)/ 空工作区占位内容。
110
+
111
+ ---
112
+
113
+ ## C. 召回重排:proof_count × recency 半衰期
114
+
115
+ ### 问题与现状核实
116
+ - 现行 `util.js bm25Rank` **已含** recency 半衰期因子 `1 + 0.5·exp(-ageDays/14)`(14 天、最多 +50%,
117
+ 不覆盖强词法相关),也有 tag-exact/fuzzy/phrase/BM25 权重——真正缺失的是**证据强度信号**:
118
+ 同样词频的记忆,被证明过 N 次的(hits)没有被排序优待。
119
+ - D2 失败复活已在 error 记忆重复时递增 `hits`;它就是现成的 proof 计数。
120
+
121
+ ### 实现(已落地:`lib/rerank.js` + `util.js bm25Rank` 接线)
122
+ ```
123
+ score_final = score_lexical // BM25 + tag/phrase boost(既有)
124
+ × (1 + 0.5·exp(-ageDays/14)) // recency 半衰期(既有)
125
+ × evidenceBoost(hits) // 新增:1 + 0.1·min(max(0,hits), 5),饱和 ±5%
126
+ ```
127
+ - `evidenceBoost(hits, {alpha=0.1, cap=5})`:0→1.0、5→1.5(封顶),保守不喧宾夺主(借鉴 reranking.py 的 proof alpha)。
128
+ - `scoreCandidate(matchScore, hits)`、`sortByEvidence(list, opts)`(稳定、可注入 tiebreak)。
129
+ - 接入点:`bm25Rank` 词法分后乘 `evidenceBoost(r.hits ?? 0)`——encerence recall/searchMemories 全走这里,零额外改动。
130
+ - 「失败复活」天然增强:高 hits 的失败记忆在同词频下与新鲜记忆竞争更接近。
131
+
132
+ ### 测试
133
+ `test/rerank.test.mjs`:三种 matchKind 基准 / 半衰期形状(t=0, halflife, 2*halflife 分别为 1, 0.5, 0.25)/ proof cap / 稳定排序(同分保序)/ 未来时间戳钳制。
134
+
135
+ ---
136
+
137
+ ## 分阶段计划(每阶段:代码 + 测试 + client-hmr 即时 / host 待重启 + 中文总结)
138
+
139
+ | 阶段 | 内容 | 交付物 | 生效 |
140
+ |---|---|---|---|
141
+ | C | rerank 纯函数 + 接入 esr_recall | `lib/rerank.js` + `test/rerank.test.mjs` | 等自然重启 |
142
+ | A | observations 表 + concentrate/trend + recall 注入 + board 观测条 | `lib/obs.js` + `lib/store.js` 钩子 + `test/obs.test.mjs` + board | host 待重启;board 即时 |
143
+ | B | mental 缓存 + esr_model 工具 + /model 端点 + board 常驻条 | `lib/mental.js` + `test/mental.test.mjs` + board | host 待重启;board 即时 |
144
+
145
+ 每阶段与既有实现正交:不动 dock 交互(除可选 KPI 一列)、不动证据门、不动 esr_close 语义。
146
+
147
+ ## 明确不做(保留约束)
148
+ - 不做 LLM 抽取/浓缩(Hindsight retain 管线)——成本与我们的形态不匹配。
149
+ - 不做向量/图库/交叉编码器重排。
150
+ - 不做 mental model 的「用户定义问题」自由文本——用固定模板,纯聚合。
151
+ - observations 的「来源逐 quote 引用」简化为 sources 列表(证据门已足够)。
152
+
153
+ ## 风险
154
+ - 浓缩误并(Jaccard 阈值误命中)→ 反证机制 + GC 可兜;阈值可配。
155
+ - observation 桶增长失控 → 每 ws 上限(默认 50 条),超出按 proof 降序逐出(保留计数进 audit),归入 esr_gc。
156
+ - mental 缓存脏标记漏置 → `bumpDirty` 挂全量写钩子 + 过期兜底(10min 强制重算)。