dsh-memu-memory 1.1.3 → 1.2.1

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.
Files changed (184) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +39 -6
  3. package/README.zh.md +39 -7
  4. package/docs/active-memory-ab.md +385 -0
  5. package/docs/binding-lifecycle.md +99 -0
  6. package/docs/contributing.md +3 -3
  7. package/docs/emotion-model-spec.md +281 -0
  8. package/docs/env-and-commands.md +8 -0
  9. package/docs/host-api.md +2 -2
  10. package/docs/memory-semantics.md +12 -10
  11. package/docs/recall-benchmark.md +238 -0
  12. package/docs/troubleshooting.md +396 -395
  13. package/lib/active-memory.js +165 -0
  14. package/lib/activity-log.js +23 -6
  15. package/lib/binding-diagnostics.js +8 -3
  16. package/lib/client.js +335 -35
  17. package/lib/consolidation-ledger.js +347 -0
  18. package/lib/consolidation-receipt.js +284 -0
  19. package/lib/consolidation-snapshot.js +274 -0
  20. package/lib/consolidation-worker.js +144 -0
  21. package/lib/curator.js +86 -31
  22. package/lib/cycle.js +40 -6
  23. package/lib/graph.js +139 -5
  24. package/lib/index.js +33 -1
  25. package/lib/inject.js +104 -76
  26. package/lib/local-embedding-manager.js +24 -6
  27. package/lib/memory.js +534 -88
  28. package/lib/mood.js +268 -39
  29. package/lib/preset-scope.js +1 -1
  30. package/lib/private.js +430 -43
  31. package/lib/prompts/private-consolidation.coding.md +1 -0
  32. package/lib/prompts/private-consolidation.md +1 -0
  33. package/lib/prompts/private-consolidation.mixed.md +1 -0
  34. package/lib/prompts/private-consolidation.rp.md +1 -0
  35. package/lib/prompts.js +4 -4
  36. package/lib/remote-summary.js +51 -25
  37. package/lib/rpc.js +54 -5
  38. package/lib/state.js +29 -12
  39. package/lib/text-similarity.js +106 -0
  40. package/lib/tools.js +113 -10
  41. package/lib/vector-migration.js +28 -3
  42. package/lib/vector-profile.js +51 -0
  43. package/package.json +4 -2
  44. package/scripts/acceptance-local.mjs +2 -1
  45. package/scripts/active-memory-ab-live.mjs +655 -0
  46. package/scripts/active-memory-ab.mjs +1302 -0
  47. package/scripts/chain-matrix.mjs +11 -1
  48. package/scripts/check-all.mjs +10 -1
  49. package/scripts/replay.mjs +573 -0
  50. package/tests/activation.test.mjs +89 -0
  51. package/tests/active-memory-ab.test.mjs +745 -0
  52. package/tests/active-memory.test.mjs +129 -0
  53. package/tests/activity-log.test.mjs +126 -0
  54. package/tests/activity-wiring.test.mjs +103 -0
  55. package/tests/appendix-b.test.mjs +216 -0
  56. package/tests/appendix-b2.test.mjs +166 -0
  57. package/tests/audit-regression.test.mjs +272 -0
  58. package/tests/audit-safety.test.mjs +227 -0
  59. package/tests/backend-compensation.test.mjs +272 -0
  60. package/tests/backup.test.mjs +793 -0
  61. package/tests/binding-diagnostics.test.mjs +720 -0
  62. package/tests/browser.test.mjs +366 -0
  63. package/tests/client-render.test.mjs +892 -0
  64. package/tests/coalesce.test.mjs +113 -0
  65. package/tests/config.test.mjs +159 -0
  66. package/tests/consolidation-credential.test.mjs +331 -0
  67. package/tests/consolidation-feed.test.mjs +96 -0
  68. package/tests/consolidation-fork.test.mjs +489 -0
  69. package/tests/consolidation-ledger.test.mjs +298 -0
  70. package/tests/consolidation-snapshot.test.mjs +360 -0
  71. package/tests/consolidation-value.test.mjs +89 -0
  72. package/tests/context-scope.test.mjs +40 -0
  73. package/tests/cue.test.mjs +44 -0
  74. package/tests/curator-commit.test.mjs +277 -0
  75. package/tests/curator-probe.test.mjs +433 -0
  76. package/tests/curator.test.mjs +150 -0
  77. package/tests/cycle.test.mjs +411 -0
  78. package/tests/disclosure.test.mjs +231 -0
  79. package/tests/doc-drift.test.mjs +222 -0
  80. package/tests/embedding-catalog.test.mjs +266 -0
  81. package/tests/embedding-download.test.mjs +391 -0
  82. package/tests/embedding-runtime.test.mjs +389 -0
  83. package/tests/emotion-chain.test.mjs +84 -0
  84. package/tests/emotion-contract.test.mjs +445 -0
  85. package/tests/engine.test.mjs +35 -0
  86. package/tests/fixtures/active-memory-ab/README.md +56 -0
  87. package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
  88. package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
  89. package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
  90. package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
  91. package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
  92. package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
  93. package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
  94. package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
  95. package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
  96. package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
  97. package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
  98. package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
  99. package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
  100. package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
  101. package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
  102. package/tests/fixtures/active-memory-ab/manifest.json +199 -0
  103. package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
  104. package/tests/fixtures/recall-bench/02-exact.json +153 -0
  105. package/tests/fixtures/recall-bench/03-negation.json +173 -0
  106. package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
  107. package/tests/fixtures/recall-bench/05-conflict.json +194 -0
  108. package/tests/fixtures/recall-bench/06-boundary.json +273 -0
  109. package/tests/fixtures/recall-bench/07-secret.json +191 -0
  110. package/tests/fixtures/recall-bench/08-coding.json +164 -0
  111. package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
  112. package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
  113. package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
  114. package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
  115. package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
  116. package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
  117. package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
  118. package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
  119. package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
  120. package/tests/fixtures/repetition-eval/02-en.json +97 -0
  121. package/tests/fixtures/repetition-eval/03-code.json +91 -0
  122. package/tests/fixtures/repetition-eval/04-names.json +116 -0
  123. package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
  124. package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
  125. package/tests/graph-cascade.test.mjs +190 -0
  126. package/tests/graph-view.test.mjs +187 -0
  127. package/tests/graph.test.mjs +94 -0
  128. package/tests/hook-order.test.mjs +72 -0
  129. package/tests/humanlike.test.mjs +232 -0
  130. package/tests/hybrid-recall.test.mjs +122 -0
  131. package/tests/inbound.test.mjs +393 -0
  132. package/tests/inject-guide.test.mjs +364 -0
  133. package/tests/inject.test.mjs +222 -0
  134. package/tests/injection-budget-settings.test.mjs +35 -0
  135. package/tests/injection-budget.test.mjs +64 -0
  136. package/tests/injection-chain.test.mjs +191 -0
  137. package/tests/loader-register.mjs +3 -0
  138. package/tests/loader.mjs +12 -0
  139. package/tests/local-embedding-entry.test.mjs +95 -0
  140. package/tests/local-embedding-manager.test.mjs +476 -0
  141. package/tests/local-embedding-protocol.test.mjs +95 -0
  142. package/tests/local-embedding-worker.test.mjs +346 -0
  143. package/tests/managed-memory.test.mjs +591 -0
  144. package/tests/managed-runtime.test.mjs +40 -0
  145. package/tests/media.test.mjs +132 -0
  146. package/tests/migration-payload.test.mjs +42 -0
  147. package/tests/mood.test.mjs +109 -0
  148. package/tests/multiprocess.test.mjs +29 -0
  149. package/tests/new-session-gate.test.mjs +361 -0
  150. package/tests/package-shipping.test.mjs +84 -0
  151. package/tests/preset-scope.test.mjs +212 -0
  152. package/tests/prompts.test.mjs +250 -0
  153. package/tests/react-snapshot.test.mjs +23 -0
  154. package/tests/recall-bench.test.mjs +306 -0
  155. package/tests/recall-dedup.test.mjs +417 -0
  156. package/tests/remote.test.mjs +140 -0
  157. package/tests/rename.test.mjs +30 -0
  158. package/tests/repetition-detector-snapshot.test.mjs +231 -0
  159. package/tests/repetition-eval-live.test.mjs +177 -0
  160. package/tests/repetition-eval.test.mjs +373 -0
  161. package/tests/salient-date.test.mjs +142 -0
  162. package/tests/settings-beat.test.mjs +221 -0
  163. package/tests/short-term.test.mjs +166 -0
  164. package/tests/sidebar-beat.test.mjs +212 -0
  165. package/tests/sidebar-refresh.test.mjs +123 -0
  166. package/tests/sidebar.test.mjs +70 -0
  167. package/tests/snippet.test.mjs +47 -0
  168. package/tests/source-kind.test.mjs +52 -0
  169. package/tests/state.test.mjs +150 -0
  170. package/tests/stats.test.mjs +112 -0
  171. package/tests/storage.test.mjs +125 -0
  172. package/tests/stubs/dsh-llm.js +5 -0
  173. package/tests/stubs/dsh-session.js +7 -0
  174. package/tests/stubs/dsh-tools.js +101 -0
  175. package/tests/tool-schema.test.mjs +73 -0
  176. package/tests/tools.test.mjs +184 -0
  177. package/tests/ui-defaults.test.mjs +57 -0
  178. package/tests/update-fields.test.mjs +138 -0
  179. package/tests/vector-migration.test.mjs +790 -0
  180. package/tests/vector-profile.test.mjs +425 -0
  181. package/tests/visibility-migration.test.mjs +152 -0
  182. package/tests/webui-auth.test.mjs +454 -0
  183. package/tests/webui.test.mjs +165 -0
  184. package/tests/windows-env.test.mjs +89 -0
@@ -0,0 +1,281 @@
1
+ # 情绪模型改造方案:四项构成与双辅助轴
2
+
3
+ 状态:**已实施(P0–P3)**,WebUI 只读展示随 P2 落地
4
+ 版本:v3(2026-10-30)
5
+ 适用范围:私有记忆的本地情绪读取、记忆写入与召回排序;不扩展全局记忆。
6
+
7
+ > 本文仍是目标契约:实现应与本文一致;不一致时以本文为准并修实现。实施结果与三处已修正的偏差见 §10。
8
+
9
+ ## 1. 目标与边界
10
+
11
+ ### 1.1 目标
12
+
13
+ 1. 在不调用模型的前提下,把当前单一 valence 扩展为四项情绪构成、arousal 和 dominance。
14
+ 2. 保留现有 valence 读写兼容性,并为旧条目提供确定的降级路径。
15
+ 3. 让情绪对召回的影响有界、可关闭、可回归测试。
16
+ 4. 让后台整理、远程整理和前台写入明确区分情绪来源。
17
+
18
+ ### 1.2 非目标
19
+
20
+ - 不给全局记忆增加情绪字段。
21
+ - 不把情绪写入注入文本或记忆正文。
22
+ - 不改变 peak、confidence、accuracyOf、negativityBias 的既有语义。
23
+ - 不引入模型调用或真实模型标注作为运行时依赖。
24
+ - 不在本阶段制作 WebUI 编辑控件;读取展示可以在后续阶段加入。
25
+
26
+ ## 2. 当前实现事实
27
+
28
+ 在开始实现前,以代码为准的现状是:
29
+
30
+ | 位置 | 当前行为 |
31
+ |---|---|
32
+ | lib/mood.js:29-71 | readMood、moodDecay、moodCongruence 只处理 valence/arousal |
33
+ | lib/state.js:893-900 | 会话 mood 只保存 valence/arousal/at |
34
+ | lib/memory.js:1135-1141 | 条目只写入 moodValence/arousal |
35
+ | lib/tools.js:190-193,309-320 | 工具尚无 emotion/dominance 参数 |
36
+ | lib/prompts.js:85-90 | 远程 JSON 契约尚无 emotion 字段 |
37
+ | lib/remote-summary.js:15-40 | 远程解析只保留现有 mood 字段 |
38
+ | lib/rpc.js:87-90 | RPC 只投影 moodValence/arousal |
39
+
40
+ 本文中的路径和行号只用于定位现状;实现时应按符号搜索,不依赖固定行号。
41
+
42
+ ## 3. 规范化数据契约
43
+
44
+ ### 3.1 情绪构成
45
+
46
+ 使用四个固定轴:joy、anger、sorrow、fear。
47
+
48
+ 有效的非空 emotion 满足:
49
+ - 四个键都存在;值必须是有限数。
50
+ - 每个值必须大于等于 0。
51
+ - 四项之和必须大于 0;写入前归一化到和为 1。
52
+ - 归一化误差要求 abs(sum - 1) < 1e-9。
53
+
54
+ emotion: null 是规范的中性表示。为了兼容工具调用,显式全零对象也接受,但在校验后规范化为 null;它不能被当作四轴全都有少量情绪。
55
+
56
+ 非法输入必须拒绝并在错误中包含 emotion:缺键、非有限数、负数、数组、字符串和非正和都不能静默修复。实现可对极小浮点误差使用固定容差,但不得把负数 clamp 成合法值。
57
+
58
+ ### 3.2 强度与掌控感
59
+
60
+ | 字段 | 范围 | 缺失默认 | 说明 |
61
+ |---|---:|---:|---|
62
+ | arousal | 0..1 | 0.35 | 情绪激活程度;显式 0 保留为 0 |
63
+ | dominance | -1..1 | 0 | 主动/失控程度;显式 0 保留为 0 |
64
+
65
+ 缺失和显式值必须区分:缺失使用默认值,显式 arousal: 0 或 dominance: 0 不得被 || 覆盖。
66
+
67
+ 内部存储、工具和远程协议统一使用字段 dominance,不再引入 mood_dominance。若未来必须兼容该别名,只能在输入边界映射一次,内部和输出只保留 dominance。
68
+
69
+ ### 3.3 记忆条目字段
70
+
71
+ | 字段 | 类型/范围 | 说明 |
72
+ |---|---|---|
73
+ | emotion | 四轴对象或 null | 事件的情绪构成 |
74
+ | arousal | 0..1 | 事件激活程度 |
75
+ | dominance | -1..1 | 事件中的主动/失控程度 |
76
+ | emotionSource | model/session/content/unknown | 系统审计字段 |
77
+ | moodValence | -1..1 | 派生的旧路径兼容投影 |
78
+ | peak | boolean | 保持现有语义 |
79
+
80
+ emotionSource 不属于普通用户可编辑字段:model 表示工具参数显式提供;session 表示允许继承当前会话 mood;content 表示从记忆正文读取;unknown 表示没有足够证据。
81
+
82
+ 旧条目没有 emotion 时保持原样,不从旧 valence 反推四轴。
83
+
84
+ ## 4. 本地读取算法
85
+
86
+ ### 4.1 readMood 返回值
87
+
88
+ 返回值必须继续包含 valence,并追加目标字段:
89
+
90
+ { valence, emotion, arousal, dominance }
91
+
92
+ 现有否定预处理必须保留。实现建议按句切分,在每句内取各轴的最大命中权重,再按规则合并;不得继续使用会把多个命中平均到接近中性的旧算法。
93
+
94
+ - 缓和词按所在分句生效:有点、稍微、一点点、a bit、somewhat。
95
+ - 一点都、一点也不、一点也没不属于缓和词。
96
+ - 强度词和重复标点可以提高 arousal,但必须有明确上限。
97
+ - 未命中可靠情绪词时返回 emotion: null,而不是强行生成均匀分布。
98
+
99
+ ### 4.2 dominance
100
+
101
+ 初始实现采用可解释的词表加构成先验:
102
+
103
+ dominance = clamp(lexicalDominance + 0.4 * (anger - fear), -1, 1)
104
+
105
+ 词表中存在歧义词时,不能把结果宣称为事实;dominance 只作为有界排序辅助。词表测试必须覆盖高控制、低控制、怒和惧的相反方向。
106
+
107
+ ### 4.3 派生 valence
108
+
109
+ 必须先确定并测试纯轴结果,再实现公式。v3 采用以下固定系数:
110
+
111
+ raw = 0.8 * joy - 0.8 * anger - 0.8 * sorrow - 0.8 * fear
112
+ moodValence = clamp(raw, -1, 1)
113
+
114
+ 这保证四个纯负轴都为负,纯喜为正;emotion 的构成仍由四轴本身表达,valence 只是旧路径的兼容投影。所有纯轴和代表性复合轴都必须有单元测试,不能同时保留与该结论冲突的经验公式。
115
+
116
+ ### 4.4 衰减
117
+
118
+ 情绪构成不按比例衰减;强度和掌控感衰减回基线:
119
+
120
+ fade = 0.5 ** (minutes / 90)
121
+ arousal = 0.35 + (arousal - 0.35) * fade
122
+ dominance = dominance * fade
123
+
124
+ 当 fade < 0.35 时,emotion 变为 null。边界是严格小于;fade === 0.35 仍保留构成。即使 emotion 变为 null,arousal 和 dominance 仍返回并继续衰减。
125
+
126
+ ## 5. 召回语义与兼容降级
127
+
128
+ ### 5.1 新路径
129
+
130
+ 双方都有合法 emotion 时:
131
+
132
+ overlap(p, q) = sum(min(p[i], q[i]))
133
+ moodBias = 1 + 0.3 * fab * (2 * overlap - 1)
134
+
135
+ moodBias 必须限制在 [0.7, 1.3]。dominance 双方都有有限值时:
136
+
137
+ domBias = 1 + 0.2 * (0.5 - abs(entry.dominance - context.dominance) / 2)
138
+
139
+ domBias 必须限制在 [0.9, 1.1]。任一 dominance 缺失或非法时,domBias 必须为 1,而不是传播 NaN。
140
+
141
+ ### 5.2 旧路径
142
+
143
+ 读路径必须使用唯一分流:
144
+ 1. 双方都有合法 emotion:使用 overlap;
145
+ 2. 否则双方都有可用 legacy valence:使用既有 moodCongruence/fadedValence;
146
+ 3. 否则:情绪乘数为 1。
147
+
148
+ 旧条目不反推 emotion。新字段接入召回后,必须验证旧条目、旧会话 mood 和缺失字段不会产生 NaN。
149
+
150
+ ### 5.3 门控
151
+
152
+ 门控是三条独立条件的 OR:
153
+
154
+ emotionActive = (emotion !== null && maxShare(emotion) >= 0.35) || abs(arousal - 0.35) >= 0.1 || abs(dominance) >= 0.25
155
+
156
+ emotion === null 只会关闭第一条,不会关闭 arousal 或 dominance 两条。必须测试 null+高 arousal 开启、null+高 dominance 开启、全基线关闭,以及非空低 share。
157
+
158
+ arousal intensity 代理应使用 max(arousal, maxShare(emotion), legacyValenceProxy, peak ? 1 : 0);没有 emotion 时只使用旧字段和 arousal。
159
+
160
+ ## 6. 写入、远程和 fork 语义
161
+
162
+ ### 6.1 工具参数
163
+
164
+ memu_private_remember 和 memu_private_update 使用 emotion: object|null、arousal: number、dominance: number。
165
+
166
+ 工具描述必须说明:emotion 描述记忆所述事件,不是整理时的心情;省略、null、全零的语义分别是继承、明确中性、规范化为 null。对工程事实(owner=about_project)默认使用 null。
167
+
168
+ 更新操作单独定义:省略字段表示保留原值;显式 null 清除 emotion 并将来源设为 unknown;显式对象经过同一套校验和归一化。
169
+
170
+ ### 6.2 远程 JSON
171
+
172
+ 远程协议只输出一个 JSON 对象,字段使用统一名称:
173
+
174
+ { memories: [{ title, content, emotion: null, arousal: 0.35, dominance: 0, mood_valence: 0, source: inferred, visibility: shared }] }
175
+
176
+ visibility 只允许当前代码支持的 shared 或 secret;不得再使用已废弃的 session。远程缺字段时回落到兼容默认;字段存在但非法时应记录可诊断错误并拒绝该条,不静默改成合法情绪。
177
+
178
+ ### 6.3 后台整理 fork
179
+
180
+ 后台 fork 不继承父会话的即时 mood。创建子 binding 时显式使用 mood: null,避免把整理时的心情误写成事件情绪。
181
+
182
+ 写入来源优先级:模型显式 emotion -> model;允许继承且存在 session mood -> session;从该条 content 读取出可靠情绪 -> content;否则 emotion=null、dominance=0、arousal=0.35、source=unknown。工程事实固定使用中性。
183
+
184
+ ## 7. 分期
185
+
186
+ ### P0:本地读取,保持旧 API
187
+
188
+ 只改 lib/mood.js 及其单元测试:四轴词表、校验/归一化、dominance、派生 valence、分句和衰减。返回值追加字段但保留 valence。P0 不是零召回行为变化:inject.js 会把读取结果写入 binding,arousal/valence 变化可能影响后续排序;若产品必须零变化,则 P0 必须由 feature flag 隔离新字段。
189
+
190
+ ### P1:召回
191
+
192
+ 在 lib/memory.js 接入 overlap、domBias、三腿门控和 intensity 代理;保留旧路径。补充 property tests、legacy fixtures 和 NaN 防护。
193
+
194
+ ### P2:写入与协议
195
+
196
+ 更新 state.js、memory.js、tools.js、prompts.js、remote-summary.js、rpc.js。完成 remember/list/update/recall 往返测试,确认缺失字段和旧 index 可读取。
197
+
198
+ ### P3:来源与 fork
199
+
200
+ 更新 private.js、整理提示词和来源审计;验证 fork 不继承即时 mood,验证 emotionSource 不可被普通 update 伪造。
201
+
202
+ ### P4:展示与文档
203
+
204
+ 更新 UI 只读展示、文档和 CHANGELOG。全局、图谱、备份和 active-memory A/B 不属于本功能范围。
205
+
206
+ ## 8. 验收标准
207
+
208
+ - 有效 emotion 归一后和为 1,所有分量非负。
209
+ - 缺键、负数、非有限数、数组和非法 dominance 被拒绝。
210
+ - 全零输入规范化为 null;null 不生成均匀分布。
211
+ - 四个纯轴的 derived valence 方向正确;复合轴结果可复算。
212
+ - overlap 自反、对称且在 [0,1];bias 范围始终满足上界。
213
+ - 门控测试覆盖 null、高 arousal、高 dominance 和低 share。
214
+ - decay 在 0.35 两侧有明确、稳定结果。
215
+ - readMood(...).valence 继续存在;旧 setSessionMood({ valence, arousal }) 继续可读。
216
+ - 旧条目无 emotion 时不反推四轴。
217
+ - 远程缺字段可解析,非法字段可诊断拒绝。
218
+ - fork 不继承父即时 mood;工程事实保持中性。
219
+ - remember -> list -> update -> recall 往返保留字段和来源。
220
+
221
+ 项目门禁:node --check changed JS files;npm test 必须 exit 0(允许明确列出的 skipped);node scripts/acceptance-local.mjs;npm run check;git diff --check。文档中的测试数字不是永久常量,报告必须记录本次实际结果,不得以 fail 0 替代成功退出。
222
+
223
+ ## 9. 风险与默认决策
224
+
225
+ - 词典推断不是事实识别;dominance 只能作为低权重排序信号。
226
+ - 四轴归一化会丢失强弱,强弱由 arousal 承载。
227
+ - fade < 0.35 有离散边界;实现不得私自改变契约。
228
+ - 远程模型可能输出不符合协议的 JSON;严格校验优先于尽量解析。
229
+
230
+ 默认决策:四轴为 joy/anger/sorrow/fear;保留 valence;dominance 参与 P1 召回;门控阈值 0.35;旧路径至少保留一个版本;WebUI 本轮只读;远程和工具统一使用 dominance。
231
+
232
+ ## 附录 A:初始词表与测试样例
233
+
234
+ 初始词表至少覆盖:
235
+ - joy:开心、高兴、太好了、喜欢、爱、满意、惊喜、谢谢、期待;
236
+ - anger:气、烦、讨厌、气死、烦死、离谱、凭什么、受不了、恨;
237
+ - sorrow:难过、失望、崩溃、委屈、心疼、累、困、没意思;
238
+ - fear:害怕、恐慌、焦虑、担心、紧张、慌、吓死、不敢、万一;
239
+ - compounds:尴尬、嫉妒等必须有明确词条或明确组合规则,不能只出现在测试期望中。
240
+
241
+ 复合预设是可解释的初始建议,不是硬编码事实:
242
+
243
+ | 表达 | 建议构成 | 建议 dominance |
244
+ |---|---|---:|
245
+ | 委屈 | sorrow 0.70 / anger 0.30 | -0.4 |
246
+ | 尴尬 | sorrow 0.50 / fear 0.50 | -0.5 |
247
+ | 嫉妒 | anger 0.50 / sorrow 0.30 / fear 0.20 | -0.1 |
248
+ | 心疼 | sorrow 0.80 / joy 0.20 | -0.2 |
249
+ | 不甘 | anger 0.60 / sorrow 0.40 | +0.4 |
250
+
251
+ 验收样例必须把期望写成可计算范围:分别统计主导轴准确率、null precision/recall、arousal 误差和 dominance 误差;复合情绪允许 top-2 轴集合匹配并记录容差。
252
+
253
+ ## 附录 B:实施前检查清单
254
+
255
+ - [ ] 实现并测试 normalizeEmotion、derivedValence、overlap 和门控真值表。
256
+ - [ ] 决定 P0 是否启用 feature flag;不要同时声称改 mood.js 且零召回变化。
257
+ - [ ] 明确 remember/update 的 omitted/null/zero 语义。
258
+ - [ ] 统一远程字段名和 visibility 枚举。
259
+ - [ ] 为旧 index、旧 binding、后台 fork 增加兼容测试。
260
+ - [ ] 以 npm test 退出码作为门禁,不引用过时的 878/取消测试数字。
261
+ - [x] 实现完成后再更新本文状态、版本和变更日志。
262
+
263
+ ## 10. 实施结果(2026-10-30)
264
+
265
+ | 阶段 | 落地位置 | 结果 |
266
+ |---|---|---|
267
+ | P0 | `lib/mood.js` 重写:四轴词表、`normalizeEmotion`、`derivedValence`(统一 0.8 系数)、`maxShare`、`overlap`、`dominanceBias`、`emotionActive`、分句读取、`moodDecay` 的 0.35 边界 | 完成 |
268
+ | P1 | `lib/memory.js` 召回:overlap 路径 + 旧 valence 回落、dominance 乘数、三腿门控、`arousalBias` 的强度代理纳入 `maxShare` | 完成;`moodBias` 实测边界 0.7 / 1.3 |
269
+ | P2 | `state.js` / `tools.js` / `prompts.js` / `remote-summary.js` / `rpc.js` / `client.js` | 完成;remember → list → update → recall 往返通过 |
270
+ | P3 | `private.js` fork 显式 `mood: null`;四份整理提示词按回合还原情绪 | 完成 |
271
+ | P4 | 文档与只读展示 | 完成;WebUI 编辑控件按 §1.2 不做 |
272
+
273
+ 实现期间发现并修正的三处偏差,记在这里避免重犯:
274
+
275
+ 1. **`derivedValence` 早期草稿把 anger 记成正贡献**,与"四个纯负轴都为负"冲突。统一为 `0.8 * (joy - anger - sorrow - fear)`。
276
+ 2. **`emotion: null` 曾在工具层被 `?? null` 与"省略"合并**,导致"明确中性"被会话心情覆盖。现在三态分开:`undefined` = 继承,`null` = 明确中性,全零对象 = 规范化为 null。
277
+ 3. **`dominanceBias(null, x)` 曾因 `Number(null) === 0` 返回 1.05**。现在缺失或非法一律返回 1。
278
+
279
+ 验收:`tests/emotion-contract.test.mjs`(17 条,由独立验收者按 §8 编写,不迁就实现)+ 全量 `npm test` 944 / 942 pass / 0 fail / exit 0。
280
+
281
+ 端到端链路验证覆盖 8 条:读取、写入四态来源、持久化、更新三态、召回、旧条目降级、展示与协议、远程清洗;36 项断言全通过。
@@ -22,6 +22,14 @@ execFile(process.execPath, ["--check", "lib/x.js"], { cwd, env });
22
22
 
23
23
  **源码是 CRLF**:任何补丁都按「读 → `split("\r\n").join("\n")` → 断言 `from` 命中 **1 次** → 替换 → 写回还原 `\r\n`」。命中 0 或 >1 次先停下看,不要盲写。(`tests/webui.test.mjs` 是 LF,别套 CRLF。)
24
24
 
25
+ ### 9.1.1 链路回放巡检
26
+
27
+ ```bash
28
+ npm run replay
29
+ ```
30
+
31
+ `scripts/replay.mjs`:不走测试框架,按生产代码路径端到端重放七条链路(事实级去重工具层、PTC 整理成功/失败、整理提醒与日志页数据、侧边栏提醒块真实渲染、向量计数账本+戳、MemU 记忆页分组/多选/查看跳转)。只有 MemU 引擎子进程换成内存桩,其余全是真实模块;退出码 0 = 全部检查点通过。改动整理/去重/向量计数/提醒任一链路后跑一次,比单测更接近「用户点下去会发生什么」。
32
+
25
33
  ### 9.2 本地 link 到 DSH profile
26
34
 
27
35
  `~/.dsh/profiles/<profile>/package.json` 里 `"dsh-memu-memory": "link:D:/Code/dsh-plugin/dsh-memu-memory"` 并加入 `dsh.profile.bundles`。是符号链接 → 改源码即刻生效(无 bundle 缓存)。`lstat` 验证是否 symlink(§3.10)。
package/docs/host-api.md CHANGED
@@ -68,7 +68,7 @@ ctx.inject([sidebarRightTabs], ...) + ctx.slots.register({ name: sidebar.right.p
68
68
 
69
69
  - 标准会话 `session-<redacted-standard-a>`(`agentPreset: "standard"`,21 个非 session 事件):模型请求头 `request/header` 的工具表为 **82 个工具**,首尾两次一致;其中 `memu_*` 共 **10 个**:`memu_private_list`、`memu_private_recall`、`memu_private_remember`、`memu_private_update`、`memu_private_archive`、`memu_private_link`、`memu_private_unpin`、`memu_global_list`、`memu_global_recall`、`memu_status`。同一会话中 `===== memory anchor =====` 出现 1 次,锚点内是真实私有 id。
70
70
  - 标准会话 `session-<redacted-standard-b>`(`agentPreset: "standard"`,本轮扫描时 1737 个非 session 事件):`<memory-guide>` 只对应 **1 个** plugin 消息 id(`<redacted-plugin-message-id>`),锚点 1 次。该 id 在多个快照/工具记录里重复出现是日志回放,不是重复投放。
71
- - 因此“标准会话的工具表里有 `memu_*`”“标准会话 anchor/guide 只投放一次”是**已证实**;标准会话的 guide 走 system prompt 段,不必然出现 `<memory-guide>` 消息。
71
+ - 因此“标准会话的工具表里有 `memu_*`”“标准会话 anchor/guide 只投放一次”是**已证实**。注意这是 2026-10-29 的历史快照:当前版本标准和预设都走 `<memory-guide rules="…">` 插件消息,system prompt 不再有本插件段。
72
72
 
73
73
  #### 仅机制 + mock:独立人格预设 `yuike`
74
74
 
@@ -87,6 +87,6 @@ ctx.inject([sidebarRightTabs], ...) + ctx.slots.register({ name: sidebar.right.p
87
87
  - **工具已注册 ≠ 已向当前模型暴露。** 上文的工具名单取自 `request/header.data.header.tools`;没有该字段或只有标题请求的会话,不能拿插件侧注册表冒充模型可见工具。
88
88
  - **guide 有 `<memory-guide>` ≠ 有 `memu_*`。** 正文自带“若本会话的工具列表里没有 `memu_*` 工具,请忽略本段”,它只声明规则可用性,不证明工具存在。
89
89
 
90
- 复核方法:用 `zlib.zstdDecompressSync` 逐 frame 解压 `~/.dsh/sessions/<bucket>/<id>/session.v4.jsonl.zstd`;只统计 `request/header` 的工具名、`session.v4` 消息 id 和 `<memory-guide>`/`===== memory anchor =====` 标签。不要打印会话正文,不要把同一消息在快照中的重复出现计成多次投放。
90
+ 复核方法:用 `zlib.zstdDecompressSync` 逐 frame 解压 `~/.dsh/sessions/<bucket>/<id>/session.v4.jsonl.zstd`;只统计 `request/header` 的工具名、`session.v4` 消息 id 和 `<memory-guide`/`===== memory anchor =====` 标签。不要打印会话正文,不要把同一消息在快照中的重复出现计成多次投放。
91
91
 
92
92
  ---
@@ -28,22 +28,23 @@
28
28
 
29
29
  ### 8.4 注入
30
30
 
31
- - 静态 guide 默认在 system prompt(按 domain 选变体);动态记忆走 plugin 消息。`persona` 带 `complete: true` 的预设会把 system prompt 段整段丢弃,这类会话改由**一次性消息**投放:`withGuideMessage`(`lib/inject.js:703`)按 `binding.guideMessageAt` 在**首步**追加一条 `<memory-guide>` 插件消息,会话内只发一次(`:708` 只在 `step === 1` 投放,`:719` 发完写 binding;fail-open,`:721`);投放的是静态规则,动态记忆仍然不进 system prompt。
31
+ - 静态 guide 与动态记忆都走 plugin 消息;当前版本不再注册本插件的 system prompt 段。所有会话(含 `persona` `complete: true`)统一由 `lib/inject.js` 的 `ensureUnifiedGuide` 在**回合首步**投放一条 `<memory-guide rules="…">` 插件消息;`rulesKey` 由协议版本、domain 和实际渲染正文计算,domain/覆盖/默认稿变化或压缩丢失(`guideLost`)时在下一个合法首步替换,同一规则不逐回合重复。旧 binding 只有 `guideMessageAt`、没有 `guideRulesKey` 时视为旧版,补投一次;失败 fail-open,不写新版本。
32
32
  - 首轮锚点 + 每用户回合最多一次;长工具回合仅有新的真实人类消息批次时追加一次,普通工具步骤不触发。私有优先、全局补充;冲突时私有胜。
33
33
  - **`injectedIds` 只能记真正进入文本的条目**,否则从未展示的记忆被白白压制 8 轮。
34
34
  - 锚点分段预算 pin 40% / core 30% / global 30%;pin 装不下时末尾明示「另有 N 条未展开」,**不静默丢弃**。
35
35
  - 预算单位是**模型 token**不是字符:中文 ≈1 字符/token、英文 ≈3.4,一律走 `lib/budget.js`,**不要再用 `tokens * 3`**(英文口径,中文超支约 3 倍)。
36
36
  - 压缩后下一条用户消息重注入锚点;子代理不注入。文件夹两开关都关 → 不注入(看 clamp 后生效值,不是 binding 原始值)。召回为空必须留信号(§1.10)。
37
- - **召回排序的准确口径**:`privateWeighted`(`lib/memory.js:643`)只服务**私有召回流水线**(`privateRecall` 调用它);全局召回(`globalRecall`,`lib/memory.js:1526`)走 `localRecall`,不经过它。cue 会经 `cueRecall`(`lib/inject.js:124`)**间接**调用 `privateRecall`,所以**不能**概括成「cue 不走私有排序」。心境一致性与唤起窄化**默认关闭**(`moodEnabled: false`,`lib/memory.js:645`),只有 `|valence| ≥ 0.15` 或 `|arousal − 0.35| ≥ 0.1` 才打开(`lib/memory.js:1110`,阈值见 `:75` 与 `lib/mood.js:27`),平缓对话里两项乘数恒为 1。`privateRecall` 的 topK 兜底是「传入 topK → `settings.privateTopK`(默认 5)→ 4」(`lib/memory.js:1107`):引用 3 或 4 时必须说清是**设置默认值**还是**代码兜底值**,两个数不是同一个。
37
+ - **召回排序的准确口径**:`privateWeighted` 只服务**私有召回流水线**(`privateRecall` 调用它);全局召回(`globalRecall`)走 `localRecall`,不经过它。cue 会经 `cueRecall` **间接**调用 `privateRecall`,所以**不能**概括成「cue 不走私有排序」。排序里的心境项默认关闭——`privateWeighted` 自己的缺省上下文就是 `moodEnabled: false`,真正打开它的是 `privateRecall`,条件是 `|valence| ≥ MOOD_MIN_STRENGTH` 或 `|arousal − BASE_AROUSAL| ≥ 0.1`(`BASE_AROUSAL` 来自 `lib/mood.js`);平缓对话里两项乘数恒为 1。`privateRecall` 的 topK 兜底是「传入 topK → `settings.privateTopK`(默认 5)→ 4」:引用 3 或 4 时必须说清是**设置默认值**还是**代码兜底值**,两个数不是同一个。
38
+ - **情绪模型(v3)**:一条记忆的情绪由四项构成 `joy / anger / sorrow / fear`(归一后和为 1)、强度 `arousal` 与掌控感 `dominance` 组成,`moodValence` 只是四轴在旧路径上的兼容投影。门控是三条**独立**条件的 OR:主导项 share ≥ 0.35、`|arousal − 0.35| ≥ 0.1`、`|dominance| ≥ 0.25`——「没有情绪构成」只关掉第一条,高唤起或明确的掌控感仍会打开排序项。双方都有合法 emotion 时用 overlap(各轴取较小值再求和)算一致性,任一侧缺失就回落到旧的 valence 路径;旧条目不从 valence 反推四轴。情绪只影响排序权重,不写进注入文本,也不进记忆正文。
38
39
 
39
40
  ### 8.5 提示词管理
40
41
 
41
42
  - 默认稿在 `lib/prompts/*.md`(随包);用户覆盖在 `~/.dsh/dsh-memu-memory/prompts/*.md`;`manifest.json` 记 `defaultHash / userHash / defaultUpdated`。运行时统一 `lib/prompts.js` 的 `loadPrompt(key, vars)` / `loadPromptForDomain(key, vars, domain)`,**业务代码里不许硬编码正文**。
42
- - system prompt 的 section `text` 必须是函数(每次装配重读文件,保存后下一轮生效)且**整体包 try/catch**(§3.7)。
43
+ - 规则消息每次首步重新渲染;`rulesKey` 只含版本、domain 和渲染后的正文,不含时间/回合,避免每轮不同。`private-policy` 模板保留供查阅和覆盖记录,但当前版本不再自动投放主会话;字段契约由工具 schema 与运行校验提供。
43
44
  - 模板只做单趟 `{{variable}}` 替换,变量值里的 `{{` 不会被二次展开——所以把记忆正文当变量塞进模板是安全的。**改成多趟插值前先想清楚这点。**
44
45
  - 恢复默认 = 只删用户覆盖;全部恢复 = 清空覆盖目录。升级后:无覆盖用新默认,有覆盖保留并标 `defaultUpdated`。
45
46
  - 分层:**作用域 → 类型页 → 项**。类型页是数据(`settings.promptDomains`,默认 `[rp, coding, mixed]` 全启用;`domainOverride` 默认 `rp`),`general` 内置不可删。变体 `<name>.<domain>.md` 由 `resolveDef` 解析:domain 在列表内且变体文件存在(默认**或**覆盖目录)→ 用变体,否则回落基础稿。
46
- - **职责分工**:`system-guide.*` 只写领域行为、检索时机、回答口径;字段定义、层级、复核、预算、unpin 只写在 `private-policy.*`。同一规则不要两份都写——重复撑长常驻提示词,改一处还留下另一半旧口径。
47
+ - **职责分工**:`system-guide.*` 是唯一常驻规则,只写领域行为、检索时机、回答口径;`private-policy.*` 保留为字段规约参考,不再投放,避免两套常驻文本互相漂移。
47
48
  - **只有三个 key 分域**:`system-guide`、`private-policy`、`private-consolidation`;`global-curator` 不分域。
48
49
  - **远程与本地共用一份 `private-consolidation`**:正文讲「记什么、字段怎么填」,差异只有输出协议(`CONSOLIDATION_CONTRACTS.local` 调工具 / `.remote` 返回 JSON,代码追加)。不要拆第二份。
49
50
  - RP 与 coding 是**内容**差异:coding 记事实(决定、约束、踩过的坑、工程偏好),召回当「可能过期的事实」,与代码冲突时以代码为准;rp 记经验(具体瞬间、半句原话、身体感觉、没说出口的话),允许模糊记错,不确定就标低 `confidence`。
@@ -88,10 +89,11 @@
88
89
  ### 8.9 空闲整理:什么时候跑,以及为什么可以什么都不写
89
90
  - 触发:`settings.sleepConsolidation` 开着时,每 5 分钟扫一遍(`lib/cycle.js` 的 `runSleepPass`),挑「`lastTurnAt` 早于 `lastSleepAt` 且已经安静 ≥ `sleepAfterIdleMinutes`」的会话,交给 `requestPrivateConsolidation` 起一个 fork 去读转录、写记忆。**默认 3 分钟**(`lib/state.js` 的 `DEFAULT_SETTINGS`);旧值 8/20 的安装保留原选择,只是新装默认更短。
90
91
  - 应用启动后 8 秒补跑一次(`STARTUP_SWEEP_MS`),因为定时器只在应用开着时存在:一段对话如果是在应用关着的时候结束的,它永远等不到那个「空闲窗口」——用户一回来发消息,`lastTurnAt` 就被顶新了。补偿每次最多处理 **2 个**会话(`runSleepPass` 的 `limit`),按最近活动排序。
91
- - **有新回合 ≠ 该写记忆。** 进 fork 之前先做一次本地评估(`lib/private.js` 的 `assessConsolidationValue`,只看 `lastSleepSeq` 之后的转录):①没有任何人类消息(只有插件通知、工具结果、心跳)→ 跳过;②只有一条**纯应答**(「谢谢」「好的」「ok」「收到」,且没有任何工具调用)→ 跳过。**这里刻意不按字数判断**:字数少的偏好句(「我不吃香菜」)比一千字的闲聊更该留,长度是坏代理。其余一律交给 fork——**跳过只是省一次 fork,「该不该留」仍由整理提示词判断**,它完全可以读完写 0 条。宁可多跑一次,也不要在本地猜「这条不重要」。
92
- - 评估通过但整理写了 0 条 = 正常成功(`lastSleepAt` 前移),不会每小时重来。
93
- - 会话被删:host 对 `readSession` 报 `session "…" not found`,这是**不变量**而不是瞬时故障。`runSleepPass` 一旦看到就把 binding 标 `sessionGone: true` 并清掉重试计数,此后直接跳过(以前会按 2 的幂退避、封顶 1 小时,永远重试);同名会话后来真的收到新消息时(`lib/inbound.js`)会清掉这个标记。
94
- - 手动「整理这场对话」(RPC `generatePrivate`)带 `force: true`,跳过上面那道评估:用户已经决定了。
92
+ - **有新回合 ≠ 该写记忆。** 进 fork 之前先做两道本地评估(只看 `lastSleepSeq` 之后的转录):① `assessConsolidationValue` 的硬门槛——没有任何人类消息(只有插件通知、工具结果、心跳)→ 跳过;只有一条**纯应答**(「谢谢」「好的」「ok」「收到」,且没有任何工具调用)→ 跳过。**这里刻意不按字数判断**:字数少的偏好句(「我不吃香菜」)比一千字的闲聊更该留,长度是坏代理。② `assessWake` 的加权分(`lib/active-memory.js`):`H` 真人回合、`C` 真人文字 + 助手正文 + 去重后的成功工具结果(每个结果最多 2000 字符)、`U` 去重后的有效工具单元;`score = 0.35*min(H/3,2) + 0.45*min(C/4000,2) + 0.20*min(U/20,2)`,默认 `activeMemoryMinScore = 0.8`。空闲未到 `activeMemoryStarvationMinutes`(默认 15 分钟)时分数必须达标;到了饥饿阈值,**小于 2000 条事件的会话**无条件唤醒,**大 transcript 不吃饥饿豁免**——fork 的 seed 是整段前缀(真机 5913 条事件约 26MB),宿主虽然会压缩上下文,实测一次整理仍要约 4~22 万 input tokens(含缓存读,主要取决于记忆工具步数);一个「继续」级薄回合只有 0.14~0.43 分,不能靠饥饿反复付这笔钱;大会话里的密集回合仍按分数正常触发。
93
+ - **近期触发过记忆就不 fork。** `runSleepPass` 先看 `suppressionReason`:距上次整理不足 `activeMemoryCooldownMinutes`(默认 0)或距上次**注入过记忆**不足 `activeMemoryRecallCooldownMinutes`(默认 15 分钟)→ 跳过并发 `check` 日志。被抑制或低分跳过的回合返回 `settled: false`,不推进 `lastSleepSeq`/`lastSleepAt`;下一轮扫描重新评估,等真正有密集新内容再整理。
94
+ - 评估通过但整理写了 0 条 = 正常成功(`lastSleepAt` 前移),不会每小时重来。整理日志额外记录 `scanned/fresh/index +N`(扫描多少事件、上次游标后新增多少、索引实际新增多少),`check` 日志记录 `score/reason/parts/空闲分钟/会话事件数`,用来区分「没触发、被门挡住、提取空转」。
95
+ - 会话被删:host 对 `readSession` 报 `session "…" not found`,这是**不变量**而不是瞬时故障。`runSleepPass` 一旦看到就把 binding 标 `sessionGone: true` 并清掉重试计数,此后直接跳过(以前会按 2 的幂退避、封顶 1 小时,永远重试);同名会话后来真的收到新消息时(`lib/inbound.js`)会清掉这个标记。**"读不出来"不是"被删了"**:转录读失败(例如 `ENOENT`)由快照层显式标成可重试(`lib/consolidation-snapshot.js` 的 `failed()` → `retryable: true`),`runSleepPass` 只按退避重试、不打 `sessionGone`——`isGoneSessionError` 只认宿主那句 `session "…" not found` / `no such session`。
96
+ - 手动「整理这场对话」(RPC `generatePrivate`)带 `force: true`,跳过上面两道评估与抑制:用户已经决定了。
95
97
 
96
98
  ---
97
99
 
@@ -101,9 +103,9 @@
101
103
 
102
104
  插件侧的处理(`lib/preset-scope.js`):每 10 秒枚举 `ctx.get('agents').list()`,沿 `agent.ctx.fiber` 往上找本插件的 fiber——找不到(= 不在自己这棵树里)才在该 agent 自己的 ctx(`lib/preset-scope.js:71` 取 `raw?.ctx || agent?.ctx || agents.get(sessionId)?.ctx`)上补挂 `registerInjection(actx, { guide: false, guideMessage: true })`(`lib/preset-scope.js:79`)。**首参必须是这个 agent 自己的 ctx**,不是部署级实例的 ctx,否则绑定与注入会挂错会话。`guide: false` 只表示**不注册 system prompt 的 guide 段**(预设未列本插件时没有 `memu_*` 工具;即便列了,`persona` 的 `complete: true` 也会丢弃该段),规则改由 `guideMessage: true` 用一次性消息投放,正文自带「工具列表里没有 `memu_*` 就忽略本段」。同树的正常会话跳过,避免注入两遍;agent 消失就卸载。
103
105
 
104
- **提示词怎么办**:`persona` 常带 `complete: true`——装配完成后它会被恢复为唯一的 system prompt 段,本插件 `systemPrompt.section()` 注册的 guide 会被整段丢弃(实测 RP 会话的 `system/message` 只有人格正文)。所以预设会话改用**消息**投放规则:首步追加一次 `<memory-guide>` 插件消息(`withGuideMessage`,按 `binding.guideMessageAt` 会话内只发一次),并注明「工具列表里没有 `memu_*` 就忽略本段」。工具确实存在的预设会话因此重新拿到主动性规则,而不是只有一个 anchor。
106
+ **提示词怎么办**:所有会话现在都走同一条消息路径,预设的 `complete: true` 不再造成投放差异;`ensureUnifiedGuide` 在回合首步追加 `<memory-guide rules="…">`,文末提示「工具列表里没有 `memu_*` 就忽略本段」。切域、改稿、默认稿升级和压缩丢失由 `rulesKey`/`guideLost` 处理;旧会话只有 `guideMessageAt` 时补投一次。
105
107
 
106
- **边界要分三件事讲**:① **注入监听**——补挂到 agent 自己的 ctx 后,锚点与召回上下文能送达;② **guide 投放**——首步一条 `<memory-guide>` 消息,会话内只发一次,正文自己声明「没有工具就忽略」;③ **最终工具面**——由该预设自己的 `cordis.yml` 清单与宿主 scope 决定,本插件**不能**从「有没有列上它」推断出一定有或一定没有。历史记录里曾出现过某些 RP 会话工具链含 `memu_private_*`,但 2026-10-29 复核无法把它归到本机 `yuike` bundle(该 bundle 未列本插件,且本机无 `agentPreset=yuike` 的真机会话),只能保留为未复核历史观察;**逐预设的真实工具名单仍须真机核实(W3)**,核实前不要写成「只有注入、没有工具」,也不要反过来写成「列上插件就一定可用」。真要在没有工具的会话里写记忆,进一次标准会话仍是最直接的办法——但如果该预设确实列了本插件且工具注册成功,记忆可以直接在那个会话里写。**不要为了这个去改别人的人格插件**:预设文件属于它的维护者,插件只能在自己的 scope 里做适配。
108
+ **边界要分三件事讲**:① **注入监听**——补挂到 agent 自己的 ctx 后,锚点与召回上下文能送达;② **guide 投放**——首步一条 `<memory-guide>` 消息,规则变化或压缩丢失时替换,不是每回合重复;正文自己声明「没有工具就忽略」;③ **最终工具面**——由该预设自己的 `cordis.yml` 清单与宿主 scope 决定,本插件**不能**从「有没有列上它」推断出一定有或一定没有。历史记录里曾出现过某些 RP 会话工具链含 `memu_private_*`,但 2026-10-29 复核无法把它归到本机 `yuike` bundle(该 bundle 未列本插件,且本机无 `agentPreset=yuike` 的真机会话),只能保留为未复核历史观察;**逐预设的真实工具名单仍须真机核实(W3)**,核实前不要写成「只有注入、没有工具」,也不要反过来写成「列上插件就一定可用」。真要在没有工具的会话里写记忆,进一次标准会话仍是最直接的办法——但如果该预设确实列了本插件且工具注册成功,记忆可以直接在那个会话里写。**不要为了这个去改别人的人格插件**:预设文件属于它的维护者,插件只能在自己的 scope 里做适配。
107
109
 
108
110
  ### 8.11 全局整理的间接输入边界
109
111
 
@@ -0,0 +1,238 @@
1
+ # 检索 benchmark(W6,provisional)
2
+
3
+ > **状态:provisional baseline,不是金标。** 本目录的 42 条样本全部是**合成草稿(synthetic draft)**,没有经过人工确认,不能称为 gold set。本文的分数只说明当前 local-lexical 管线在一组刻意构造的样本上的行为,不能代表真实 RP 对话或语义检索质量。
4
+
5
+ - 工作包:W6 · 检索 benchmark(P1,W7 的前置)
6
+ - 基线时间:2026-10-29T09:39:30Z(测试运行时钟)
7
+ - 基线提交:`afc970f934df`(HEAD;W6 文件尚未提交,工作区为 dirty)
8
+ - 包版本:`dsh-memu-memory@1.1.2`
9
+ - 数据集:42 条 / 8 个文件,SHA-256 `4b3d01898071073f318d815346b1af2758878900a2685355f6bd40445531a2b0`
10
+ - 后端:`local-lexical`;没有启用真实 embedding 或真实向量服务
11
+
12
+ ## 1. 结论(只读这一节)
13
+
14
+ 本次 provisional baseline:
15
+
16
+ | 指标 | 候选 id(candidate) | 最终注入 id(final injected) | 显式探针(explicit) |
17
+ |---|---:|---:|---:|
18
+ | Recall@3 | 1.000000 | 1.000000 | 1.000000 |
19
+ | MRR@3 | 0.984848 | 0.984848 | 1.000000 |
20
+ | 无答案误召回率 | 0.000000 | 0.000000 | n/a(无无答案样本) |
21
+ | 隔离违规 | 0 | 0 | 0 |
22
+ | 可答样本分母 | 33 | 33 | 5 |
23
+ | 无答案分母 | 9 | 9 | 0 |
24
+
25
+ 隔离违规总计 **0**。其中 forbidden id 检查次数:候选 20 + 最终注入 20 + 显式探针 0 = 40 次,命中 0 次。
26
+
27
+ 这些数字是**合成草稿上的 provisional baseline**。样本的 query 与条目文本存在刻意保留的词面线索,local-lexical 接近满分是预期现象,不是“检索质量已经解决”的证据。
28
+
29
+ ## 2. 运行方式
30
+
31
+ 脚本:`scripts/recall-bench.mjs`
32
+
33
+ ```bash
34
+ # 默认输出完整 JSON 到 stdout
35
+ node scripts/recall-bench.mjs
36
+
37
+ # 写出机器可读报告
38
+ node scripts/recall-bench.mjs --out artifacts/recall-bench.json
39
+
40
+ # 仅跑前 N 条;固定 seed;保留临时根用于检查
41
+ node scripts/recall-bench.mjs --samples 10 --seed 20261029 --work-root "$TMPDIR/recall-bench-inspect" --keep
42
+ ```
43
+
44
+ 参数:
45
+
46
+ | 参数 | 说明 |
47
+ |---|---|
48
+ | `--out FILE` | 写 JSON 报告;父目录自动创建。报告按 CRLF 写出 |
49
+ | `--work-root DIR` | 使用指定的空目录;必须是 `os.tmpdir()` 下的目录,否则拒绝运行 |
50
+ | `--seed N` | 替换 PRNG seed;默认 20261029 |
51
+ | `--samples N` | 只跑排序后的前 N 条;1..42 |
52
+ | `--keep` | 保留本次临时根(默认根在结束后删除;报告里的 `run.root` 可查看路径) |
53
+
54
+ 测试命令:
55
+
56
+ ```bash
57
+ node --import ./tests/loader-register.mjs --test tests/recall-bench.test.mjs
58
+ ```
59
+
60
+ 测试只验证 harness、样本结构、隔离、确定性和指标数学,不锁定上面的具体分数;分数变化不应让结构测试变成“追数字”。
61
+
62
+ ## 3. 合成样本集
63
+
64
+ 位置:`tests/fixtures/recall-bench/`,共 8 个 JSON 文件、42 条样本。每个 JSON 文件都带有 `status`,每条样本都带有 `draft`/`notGold`:
65
+
66
+ ```json
67
+ {
68
+ "status": "synthetic-draft-not-gold",
69
+ "draft": true,
70
+ "notGold": true
71
+ }
72
+ ```
73
+
74
+ | 子类 | 样本数 | 覆盖点 |
75
+ |---|---:|---|
76
+ | `paraphrase` | 6 | 同义改写、指代和口语化问法 |
77
+ | `exact` | 5 | 精确 title、精确 memory id |
78
+ | `negation` | 4 | 否定、旧状态不得被当成当前状态 |
79
+ | `no-answer` | 6 | 英文 query 对中文小型库,必须返回空 top-3 |
80
+ | `conflict` | 5 | 新旧冲突、pending review 不得当作已确认 |
81
+ | `boundary` | 6 | channelId / characterId / worldId / branchId / knownBy 边界 |
82
+ | `secret` | 5 | secret 自动隐藏 vs 显式召回;含 3 条自动无答案 |
83
+ | `coding` | 5 | 少量 coding 约束,防跨域退化 |
84
+ | **合计** | **42** | |
85
+
86
+ 每条的字段(脚本会逐项校验):
87
+
88
+ | 字段 | 含义 |
89
+ |---|---|
90
+ | `query` | 检索 query |
91
+ | `context` | 会话上下文(可为空对象;边界样本使用 channelId/characterId/worldId/branchId/knownBy 等) |
92
+ | `time` | 固定时钟;条目 ageDays/updatedDaysAgo 相对此时间计算 |
93
+ | `expectedIds` | 期望命中的 id;无答案样本为空数组 |
94
+ | `forbiddenIds` | 禁止出现在任何 top-3 的 id;计入隔离违规 |
95
+ | `noAnswer` | true 表示该样本必须无召回;无答案样本的 expectedIds 必须为空 |
96
+ | `subclass` | 上表八个子类之一 |
97
+ | `library` | 该样本可用的小型 library 子集(1–10 个 id),样本之间不复用 library,避免互相污染 |
98
+ | `explicitExpectedIds` | 可选;显式召回探针的期望 id(用于 secret 自动/显式差异) |
99
+ | `explicitForbiddenIds` | 可选;显式召回探针的禁止 id |
100
+
101
+ 数据是人工编写的合成内容(虚构人名、地点、约定),不包含真实聊天、真实用户记忆或可识别个人信息,可以安全入库。**它们仍不是人工确认样本。**
102
+
103
+ ## 4. 隔离与可复现设计
104
+
105
+ 每次 `runBenchmark`:
106
+
107
+ 1. 在 `os.tmpdir()` 下新建(或使用调用者提供的空)根目录;`--work-root` 也必须在 tmpdir 下。
108
+ 2. 把 `DSH_MEMU_HOME` 指向 `<root>/dsh-memu-home`,把 `DSH_MEMU_MEMORY_HOME` 指向 `<root>/dsh-memu-data`。结束后在 `finally` 中恢复原环境变量。
109
+ 3. 不读取、不打开、不写入用户真实 `dataDir()`。harness 没有“指向用户库”的参数;要评估真实库,必须先另做只读快照设计,当前未实现。
110
+ 4. 每条样本创建独立的 library(独立 `scopeKey`)和独立 session,写入独立 private index;样本声明的 `library` 子集只在本样本内可见。
111
+ 5. secret 显式探针使用第二套独立 library/session,避免 `purpose='explicit'` 的 `touchAccessMany` 访问计数影响自动召回;所有 `accessCount`、`injectCount` 都从 0 开始且只存在临时状态中。
112
+ 6. 固定时钟:`Date.now()` 被按样本 `time` 覆盖;固定随机:seeded mulberry32;默认 seed 20261029。
113
+ 7. 默认运行结束删除临时根;传 `work-root` 时保留,便于检查 `state.json`。
114
+
115
+ **隔离证据**:测试在测试自己的 `DSH_MEMU_HOME` 下放 sentinel 文件,运行两次 benchmark 后检查 sentinel 内容未变、该 home 下没有生成 `state.json`;同时检查 run root 下存在 `dsh-memu-data/state.json`,且 `metrics.isolation.violations === 0`。完整证据由测试输出和报告 JSON 共同提供,不靠口头声明。
116
+
117
+ **确定性**:同一数据 SHA-256、同一 seed、同一 config 的两次运行,候选/最终/显式指标与逐样本 top-3 完全一致(测试比较时剔除天然波动的 `latencyMs`;`startedAt` 和临时根路径也不会相同)。延迟不能承诺逐次相同。
118
+
119
+ ## 5. 指标定义
120
+
121
+ 三个投影分别计算,且都只看**前 3 个 id**:
122
+
123
+ - **候选 id**:`privateRecall({ purpose: 'automatic', topK: 20 })` 返回的 id 顺序的前 3 个。`candidateTop3` 是候选阶段指标。
124
+ - **最终注入 id**:`buildRecall(...)` 返回的 `ids` 的前 3 个。它可能受 `dedupeTurns`、`useGlobal`、token budget 和格式渲染影响,因此必须与候选分开记录。
125
+ - **显式探针**:对声明了 `explicitExpectedIds` 的样本,用独立 library/session 调 `privateRecall({ purpose: 'explicit' })` 的前 3 个;只用于 secret 自动/显式差异,不混入自动指标。
126
+
127
+ 定义与空集规则:
128
+
129
+ | 指标 | 分母 | 规则 |
130
+ |---|---|---|
131
+ | Recall@3 | `noAnswer=false` 的样本数(answerable) | 前 3 中至少有一个 `expectedIds` 即命中;命中样本数 / answerable |
132
+ | MRR@3 | 同上 | 第一个期望 id 的排名取倒数(1/rank);前 3 没有则为 0;求和 / answerable |
133
+ | 无答案误召回率 | `noAnswer=true` 的样本数 | 前 3 非空即误召回;误召回样本数 / noAnswer |
134
+ | 隔离违规 | 不设 rate 分母;报告 forbidden 检查次数 | 每个 `forbiddenIds` 在每个投影的 top-3 中出现一次记一次;必须为 0 |
135
+
136
+ - 某个分母为 0 时,对应 rate 输出 `0`,并在 `emptySet` 中标记为 `true`;不会用 0 冒充“完美”。
137
+ - `noAnswer=true` 的样本即使有 `forbiddenIds`,也不会进入 Recall/MRR 分母;其 top-3 非空同时会造成无答案误召回。
138
+ - `isolation.violations` 是 candidate、finalInjected、explicitProbe 三个投影的违规总和;`forbiddenDenominator` 是三个投影的 forbidden 检查次数总和。隔离要求是精确 0,不是“低于某个率”。
139
+
140
+ ## 6. Provisional baseline 明细
141
+
142
+ ### 6.1 总体
143
+
144
+ | 投影 | answerable | Recall@3 | MRR@3 | noAnswer | 无答案误召回率 | 隔离违规 | forbidden 检查 |
145
+ |---|---:|---:|---:|---:|---:|---:|---:|
146
+ | 候选 id | 33 | 1.000000 | 0.984848 | 9 | 0.000000 | 0 | 20 |
147
+ | 最终注入 id | 33 | 1.000000 | 0.984848 | 9 | 0.000000 | 0 | 20 |
148
+ | 显式探针 | 5 | 1.000000 | 1.000000 | 0 | n/a(空集) | 0 | 0 |
149
+
150
+ ### 6.2 按子类
151
+
152
+ 候选与最终注入在本次 baseline 中相同;下表列的是两者共同值(逐样本仍分别保存)。
153
+
154
+ | 子类 | 样本 | answerable / noAnswer | Recall@3 | MRR@3 | 无答案误召回 |
155
+ |---|---:|---:|---:|---:|---:|
156
+ | paraphrase | 6 | 6 / 0 | 1.000000 | 1.000000 | 0.000000 |
157
+ | exact | 5 | 5 / 0 | 1.000000 | 1.000000 | 0.000000 |
158
+ | negation | 4 | 4 / 0 | 1.000000 | 1.000000 | 0.000000 |
159
+ | no-answer | 6 | 0 / 6 | 0(空集) | 0(空集) | 0.000000 |
160
+ | conflict | 5 | 5 / 0 | 1.000000 | 0.900000 | 0.000000 |
161
+ | boundary | 6 | 6 / 0 | 1.000000 | 1.000000 | 0.000000 |
162
+ | secret | 5 | 2 / 3 | 1.000000 | 1.000000 | 0.000000 |
163
+ | coding | 5 | 5 / 0 | 1.000000 | 1.000000 | 0.000000 |
164
+
165
+ ### 6.3 延迟与预算
166
+
167
+ 延迟(`metrics.latency`):
168
+
169
+ - 后端:`local-lexical`
170
+ - 样本数:42
171
+ - 口径:单 Node 进程,`performance.now()` 包住每条样本的 automatic `privateRecall` + `buildRecall`;不是服务延迟,也不是网络/磁盘全链路延迟
172
+ - 本次:median **25.618 ms**,p95 **33.959 ms**
173
+ - 延迟随机器和运行波动,不属于确定性断言
174
+
175
+ Token 预算(只能称 estimate):
176
+
177
+ - 字段:`budget.estimate`(同时保留 `injectedTokenEstimateTotal` / `injectedTokenEstimateMean` 兼容字段)
178
+ - 方法:`lib/budget.js` 的 `estTokens`(CJK 约 1 token/字,Latin 约 3.4 字符/token)
179
+ - 本次:`injectedTokenTotal=4867`,`injectedTokenMeanPerSample=115.881`,`turnTokens=640`
180
+ - **这是估算,不是模型 tokenizer 的实测消耗。** 没有用真实 tokenizer;不得把它写成“实际用量”。
181
+
182
+ ## 7. 机器可读输出
183
+
184
+ `runBenchmark()` 返回 / CLI `--out` 写出的 JSON 包含:
185
+
186
+ - `schemaVersion`、`kind`、`ok`、`status`
187
+ - `dataset`:status、draft、notGold、sampleCount、fileCount、sha256、files
188
+ - `code`:git commit、packageVersion、worktree 说明
189
+ - `run`:startedAt、root、clock、randomSeed、random、sampleLimit
190
+ - `config`:backend、candidateTopK=20、privateTopK=3、turnTokens=640、relevanceThreshold=0.4、dedupeTurns=16、vectorPath
191
+ - `metrics`:candidate、finalInjected、explicitProbe、isolation、latency、bySubclass
192
+ - `budget`:`estimateOnly=true`、`estimate` 对象、flat estimate 字段与 note
193
+ - `samples`:每条样本的 `candidateIds`/`candidateTop3`、`injectedIds`/`injectedTop3`、`explicitIds`/`explicitTop3`、估计 token、延迟与状态
194
+
195
+ ## 8. 未验证项与限制
196
+
197
+ 以下都**未验证**,不能从本 baseline 推出:
198
+
199
+ 1. **人工金标**:42 条全是合成草稿,没有人工逐条核对、没有标注者一致性;不能叫 gold,也不能用作上线门槛。
200
+ 2. **真实用户库**:harness 不读取真实 `dataDir()`,没有在真实记忆上跑过;真实库的规模、噪声、重复、历史状态会改变结果。
201
+ 3. **真实语义/向量质量**:本次只有 local-lexical;`mockVectors` 仅标记管线接口,未调用真实 embedding。W7 的混合召回质量仍未验证。
202
+ 4. **模型 tokenizer / 实际 token 用量**:预算只来自字符估算;没有 tokenizer 版本或实测对照。
203
+ 5. **服务延迟**:25.618/33.959 ms 是本机单进程的 harness 耗时,不是 WebUI 或 API 后端服务延迟。
204
+ 6. **跨平台/并发**:只在当前 Windows 工作区和单进程测试;没有并发写、崩溃恢复、多个 session 同时运行的覆盖。
205
+ 7. **真实 RP 分布**:样本是人工设计的短查询,不能代表长篇 RP 的自然语言分布、长上下文和多轮注入历史。
206
+ 8. **生产排序**:本包只 import 生产函数做只读调用,没有改 `lib/memory.js`、`lib/inject.js` 或任何生产排序/权重;因此不能声称生产召回已被优化。
207
+ 9. **数据可迁移性**:样本只在小 library 子集(每条 1–10 条)内评测,不能外推到全局 library 排序。
208
+
209
+ ## 9. 回退与数据影响
210
+
211
+ - **生产代码/配置**:无改动。没有改排序、阈值、默认 backend、schema 或共享 `/api`。
212
+ - **用户数据**:无接触、无迁移、无写入。所有 benchmark 状态在 `os.tmpdir()` 的临时根下,默认运行后删除;`--work-root` 仅供调试,且仍必须在 tmpdir 下。
213
+ - **回退方式**:删除本包新增的 `scripts/recall-bench.mjs`、`tests/fixtures/recall-bench/`、`tests/recall-bench.test.mjs`、本文档即可;没有数据库或运行时状态需要回滚。
214
+ - 若要把本包提交到版本库,应先按仓库流程单独提交;当前 W6 工作区改动尚未 commit(见 `code.worktree`)。
215
+
216
+ ## 10. 人工金标候选(待审核,非 gold)
217
+
218
+ 除默认 42 条 `synthetic-draft-not-gold` 外,W6 另有一套 **40 条人工审核候选**:
219
+
220
+ - 位置:`tests/fixtures/recall-bench-gold-candidates/`(8 个文件、8 个子类各 5 条、64 个库条目)。
221
+ - 状态:`gold-candidate-pending-review`;每条 `draft=true`、`notGold=true`、`gold=false`、`review=pending`。
222
+ - 数据集 SHA-256:`c6f3a5461de357d8d6fd5a89ada3a068e82b08cb047251258abc32c9919bc8cf`。
223
+ - 审核表与逐条勾选流程由维护者本地保存,不随仓库分发(其中含候选内容)。
224
+ - 运行候选集:`node --import ./tests/loader-register.mjs scripts/recall-bench.mjs --fixture-dir tests/fixtures/recall-bench-gold-candidates`。
225
+
226
+ 候选集**不是** gold set。只有人工逐条确认、另行迁移状态并重算哈希后,才能讨论金标分数;当前测试只验证结构、隔离和 no-answer 空集,不锁定候选分数。默认数据集与默认 CLI 行为保持不变。
227
+
228
+ ## 11. W7 评测证据与生产边界
229
+
230
+ 旧 W7 混合召回设置 (`hybridRecall` / `hybridRecallMode`) 及其 LLM 重排生产接线已移除。当前 benchmark 只验证本地词法管线、候选资格/隔离、最终注入和预算估算;它不启用聊天/总结模型,也不把历史 LLM 实验当作生产路径。
231
+
232
+ 默认数据集的 42 条 synthetic-draft 样本在 2026-10-29 复跑结果与 §5 基线一致:candidate / final-injected 的 Recall@3 = 1、MRR@3 = 0.984848、no-answer 误召回 = 0、隔离违规 = 0;预算估算 `injectedTokenTotal=4867`、mean=115.881、`turnTokens=640`;数据集 SHA-256 未变 `4b3d01898071073f318d815346b1af2758878900a2685355f6bd40445531a2b0`。
233
+
234
+ 历史 W7 LLM 重排实验(2026-10-29,两轮;仅保留证据,不提供生产启用入口):同一 42 条 synthetic-draft 数据集,candidate / final-injected 的 Recall@3 = 1、MRR@3 = 0.984848、no-answer 误召回 = 0、隔离违规 = 0,两轮返回 id 序列相同。31 条走 hybrid-llm-rrf、11 条保持 local(6 条 no-answer、3 条 automatic secret 空集、2 条 exact id);55 个词法候选、32 个有效重排 id,23 个 local-only、0 个 semantic-only、32 个 shared,没有调用错误。4 条 top-3 顺序变化,top-1 不变。第一轮 median=4249.458 ms / p95=6102.986 ms,第二轮 median=4305.708 ms / p95=7804.750 ms;墙钟约 166 s / 184 s。预算仅估算:total=5096、mean=121.333、turnTokens=640(词法基线 total=4867、mean=115.881);不是实际 token 消耗,也不是有效性或上线许可。
235
+
236
+ W8(助手重复提示)已决定不做,理由与结论见 `AGENTS.md`;证据文档已删除,旧 LLM/重复提示实验与当前生产召回路径解耦,不应再通过 benchmark CLI 重跑。
237
+
238
+ 测试覆盖:`tests/hybrid-recall.test.mjs` 仅保留通用 RRF/fusion helper;managed vector 行为由 `tests/managed-memory.test.mjs` 覆盖。