dsh-memu-memory 1.1.3 → 1.2.0

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 (173) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +31 -2
  3. package/README.zh.md +30 -2
  4. package/docs/active-memory-ab.md +385 -0
  5. package/docs/binding-lifecycle.md +99 -0
  6. package/docs/contributing.md +1 -1
  7. package/docs/emotion-model-spec.md +281 -0
  8. package/docs/host-api.md +2 -2
  9. package/docs/memory-semantics.md +12 -10
  10. package/docs/recall-benchmark.md +238 -0
  11. package/docs/troubleshooting.md +395 -395
  12. package/lib/active-memory.js +165 -0
  13. package/lib/activity-log.js +1 -1
  14. package/lib/binding-diagnostics.js +8 -3
  15. package/lib/client.js +33 -11
  16. package/lib/consolidation-ledger.js +347 -0
  17. package/lib/consolidation-receipt.js +284 -0
  18. package/lib/consolidation-snapshot.js +274 -0
  19. package/lib/consolidation-worker.js +115 -0
  20. package/lib/cycle.js +40 -6
  21. package/lib/inject.js +104 -76
  22. package/lib/local-embedding-manager.js +24 -6
  23. package/lib/memory.js +221 -19
  24. package/lib/mood.js +268 -39
  25. package/lib/preset-scope.js +1 -1
  26. package/lib/private.js +335 -40
  27. package/lib/prompts/private-consolidation.coding.md +1 -0
  28. package/lib/prompts/private-consolidation.md +1 -0
  29. package/lib/prompts/private-consolidation.mixed.md +1 -0
  30. package/lib/prompts/private-consolidation.rp.md +1 -0
  31. package/lib/prompts.js +4 -4
  32. package/lib/remote-summary.js +51 -25
  33. package/lib/rpc.js +9 -1
  34. package/lib/state.js +29 -12
  35. package/lib/tools.js +49 -2
  36. package/lib/vector-profile.js +27 -0
  37. package/package.json +2 -1
  38. package/scripts/acceptance-local.mjs +2 -1
  39. package/scripts/active-memory-ab-live.mjs +655 -0
  40. package/scripts/active-memory-ab.mjs +1302 -0
  41. package/scripts/chain-matrix.mjs +11 -1
  42. package/scripts/check-all.mjs +10 -1
  43. package/tests/activation.test.mjs +89 -0
  44. package/tests/active-memory-ab.test.mjs +745 -0
  45. package/tests/active-memory.test.mjs +129 -0
  46. package/tests/activity-log.test.mjs +126 -0
  47. package/tests/activity-wiring.test.mjs +103 -0
  48. package/tests/appendix-b.test.mjs +212 -0
  49. package/tests/appendix-b2.test.mjs +166 -0
  50. package/tests/audit-regression.test.mjs +272 -0
  51. package/tests/audit-safety.test.mjs +227 -0
  52. package/tests/backend-compensation.test.mjs +272 -0
  53. package/tests/backup.test.mjs +793 -0
  54. package/tests/binding-diagnostics.test.mjs +720 -0
  55. package/tests/browser.test.mjs +366 -0
  56. package/tests/client-render.test.mjs +892 -0
  57. package/tests/coalesce.test.mjs +113 -0
  58. package/tests/config.test.mjs +159 -0
  59. package/tests/consolidation-credential.test.mjs +320 -0
  60. package/tests/consolidation-fork.test.mjs +357 -0
  61. package/tests/consolidation-ledger.test.mjs +298 -0
  62. package/tests/consolidation-snapshot.test.mjs +321 -0
  63. package/tests/consolidation-value.test.mjs +89 -0
  64. package/tests/context-scope.test.mjs +40 -0
  65. package/tests/cue.test.mjs +44 -0
  66. package/tests/curator-probe.test.mjs +433 -0
  67. package/tests/curator.test.mjs +150 -0
  68. package/tests/cycle.test.mjs +411 -0
  69. package/tests/disclosure.test.mjs +231 -0
  70. package/tests/doc-drift.test.mjs +222 -0
  71. package/tests/embedding-catalog.test.mjs +266 -0
  72. package/tests/embedding-download.test.mjs +391 -0
  73. package/tests/embedding-runtime.test.mjs +389 -0
  74. package/tests/emotion-chain.test.mjs +84 -0
  75. package/tests/emotion-contract.test.mjs +445 -0
  76. package/tests/engine.test.mjs +35 -0
  77. package/tests/fixtures/active-memory-ab/README.md +56 -0
  78. package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
  79. package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
  80. package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
  81. package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
  82. package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
  83. package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
  84. package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
  85. package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
  86. package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
  87. package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
  88. package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
  89. package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
  90. package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
  91. package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
  92. package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
  93. package/tests/fixtures/active-memory-ab/manifest.json +199 -0
  94. package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
  95. package/tests/fixtures/recall-bench/02-exact.json +153 -0
  96. package/tests/fixtures/recall-bench/03-negation.json +173 -0
  97. package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
  98. package/tests/fixtures/recall-bench/05-conflict.json +194 -0
  99. package/tests/fixtures/recall-bench/06-boundary.json +273 -0
  100. package/tests/fixtures/recall-bench/07-secret.json +191 -0
  101. package/tests/fixtures/recall-bench/08-coding.json +164 -0
  102. package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
  103. package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
  104. package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
  105. package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
  106. package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
  107. package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
  108. package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
  109. package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
  110. package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
  111. package/tests/fixtures/repetition-eval/02-en.json +97 -0
  112. package/tests/fixtures/repetition-eval/03-code.json +91 -0
  113. package/tests/fixtures/repetition-eval/04-names.json +116 -0
  114. package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
  115. package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
  116. package/tests/graph-view.test.mjs +187 -0
  117. package/tests/graph.test.mjs +77 -0
  118. package/tests/hook-order.test.mjs +72 -0
  119. package/tests/humanlike.test.mjs +232 -0
  120. package/tests/hybrid-recall.test.mjs +92 -0
  121. package/tests/inbound.test.mjs +393 -0
  122. package/tests/inject-guide.test.mjs +364 -0
  123. package/tests/inject.test.mjs +222 -0
  124. package/tests/injection-budget-settings.test.mjs +35 -0
  125. package/tests/injection-budget.test.mjs +64 -0
  126. package/tests/injection-chain.test.mjs +191 -0
  127. package/tests/loader-register.mjs +3 -0
  128. package/tests/loader.mjs +12 -0
  129. package/tests/local-embedding-entry.test.mjs +95 -0
  130. package/tests/local-embedding-manager.test.mjs +476 -0
  131. package/tests/local-embedding-protocol.test.mjs +95 -0
  132. package/tests/local-embedding-worker.test.mjs +346 -0
  133. package/tests/managed-memory.test.mjs +536 -0
  134. package/tests/managed-runtime.test.mjs +40 -0
  135. package/tests/media.test.mjs +132 -0
  136. package/tests/migration-payload.test.mjs +42 -0
  137. package/tests/mood.test.mjs +109 -0
  138. package/tests/multiprocess.test.mjs +29 -0
  139. package/tests/new-session-gate.test.mjs +361 -0
  140. package/tests/package-shipping.test.mjs +84 -0
  141. package/tests/preset-scope.test.mjs +212 -0
  142. package/tests/prompts.test.mjs +250 -0
  143. package/tests/react-snapshot.test.mjs +23 -0
  144. package/tests/recall-bench.test.mjs +306 -0
  145. package/tests/remote.test.mjs +140 -0
  146. package/tests/rename.test.mjs +30 -0
  147. package/tests/repetition-detector-snapshot.test.mjs +231 -0
  148. package/tests/repetition-eval-live.test.mjs +177 -0
  149. package/tests/repetition-eval.test.mjs +373 -0
  150. package/tests/salient-date.test.mjs +142 -0
  151. package/tests/settings-beat.test.mjs +221 -0
  152. package/tests/short-term.test.mjs +166 -0
  153. package/tests/sidebar-beat.test.mjs +208 -0
  154. package/tests/sidebar-refresh.test.mjs +123 -0
  155. package/tests/sidebar.test.mjs +70 -0
  156. package/tests/snippet.test.mjs +47 -0
  157. package/tests/source-kind.test.mjs +52 -0
  158. package/tests/state.test.mjs +150 -0
  159. package/tests/stats.test.mjs +112 -0
  160. package/tests/storage.test.mjs +125 -0
  161. package/tests/stubs/dsh-llm.js +5 -0
  162. package/tests/stubs/dsh-session.js +7 -0
  163. package/tests/stubs/dsh-tools.js +101 -0
  164. package/tests/tool-schema.test.mjs +73 -0
  165. package/tests/tools.test.mjs +184 -0
  166. package/tests/ui-defaults.test.mjs +57 -0
  167. package/tests/update-fields.test.mjs +138 -0
  168. package/tests/vector-migration.test.mjs +733 -0
  169. package/tests/vector-profile.test.mjs +425 -0
  170. package/tests/visibility-migration.test.mjs +152 -0
  171. package/tests/webui-auth.test.mjs +454 -0
  172. package/tests/webui.test.mjs +164 -0
  173. package/tests/windows-env.test.mjs +89 -0
@@ -0,0 +1,99 @@
1
+ # 绑定生命周期 — dsh-memu-memory
2
+
3
+ > 本文对应 W9 只读诊断。实现:`lib/binding-diagnostics.js`;CLI:`scripts/diagnose-bindings.mjs`;测试:`tests/binding-diagnostics.test.mjs`。诊断只读取磁盘上的持久层,不创建、不修复、不删除、不 GC、不写回。
4
+
5
+ ## 12. 绑定生命周期
6
+
7
+ ### 12.1 三层状态:哪些在磁盘上,哪些只在进程里
8
+
9
+ 一个会话的记忆状态分成三层。只有第一层能被另一个进程从磁盘读到,后两层由运行中的插件进程持有;诊断报告把后两层明确标成不可观察,而不是猜成 0。
10
+
11
+ | 层 | 内容 | 生命周期 | 诊断可见性 |
12
+ |---|---|---|---|
13
+ | 1. 持久绑定 | `state.json` 的 `sessions[sessionId]`:`scopeKey`、`libraryId`、开关、`memoryContext`、`inbound.pending/seen/claimed/clock`、`sleepRetryAt`、注入标记等 | 跨重启、fork、插件重载保留;只有显式状态写入才改变 | 只读快照可读;报告的核心对象 |
14
+ | 2. 运行时监听器 | `lib/preset-scope.js:19` 的 `attachments`,按 **agent context 对象** 键控 | 随 context 存在;context 被替换时旧监听器应释放,下一次 attach 重新挂载 | 跨进程不可读,声明为 `runtime-listener` |
15
+ | 3. 临时映射/缓存 | `lib/inbound.js:12-14` 的 `mediaJobs`/`calls`/`sessions`、`lib/media.js:19` 的 `captures`、`lib/private.js:213` 的 `inFlight`、`lib/curator.js:11` 的 `jobs`、`lib/state.js:938` 的 `indexCache`、`lib/paths.js:52` 的 `readFailures` | 随 promise settle、turn/end、session/disposed、下一次成功读取或下一次加载清理 | 跨进程不可读,声明为 `temporary-map` 或 `cache` |
16
+
17
+ **三层不混算。** `summary.bindings`、`summary.inbound`、`summary.unfinished` 只统计持久绑定;`summary.runtime` 只在调用方提供了进程内快照时出现,分别是 `runtimeListeners` 和 `temporaryMaps` 的合计。runtime 计数不会加进 binding 数,也不会把“持久层零候选”解释成“运行时没有残留”。同一份 state 在提供/不提供 runtime 快照时,`bindings` 与 `candidates` 必须完全一致。
18
+
19
+ `RUNTIME_STATE` 里的 `file:line` 是诊断契约的一部分。测试会逐条打开源文件、检查对应行确实声明了该名字;源文件移动后必须同步更新 `RUNTIME_STATE`,否则测试直接失败,避免报告印出过期位置。
20
+
21
+ ### 12.2 生命周期各阶段
22
+
23
+ - **首次绑定**:会话第一次进入记忆流程时创建 `sessions[sessionId]`。同一 `sessionId` 重连只复用这一条绑定,不按标题、不按 agent 实例、不按时间新建。文件级重复读取也必须幂等:同一 `state.json` 连续诊断两次,`bindings` 与 `candidates` 相同,字节和 mtime 不变。
24
+ - **开关打开但还没有库**:`useLibrary: true` 且 `libraryId: null` 本身不是故障。文件夹没有库时,注入路径用 `createLibrary: false` 延迟创建,这是文档化的形状。只有快照里存在“完成过注入”的证据(`injectedIds` 非空或 `lastInjectedTurn > 0`)时,才把 `library-enabled-but-unbound` 作为候选;否则只计入 `unbound-basis-unknown` 未知项,不虚增候选。
25
+ - **ctx 替换**:agent context 被替换只影响第 2 层监听器,不应改写 `sessions[sessionId]`。替换前后持久绑定的 `bindings`、队列、作用域和候选必须相同;变化只出现在 runtime 的 `presetAttachments` 计数上。
26
+ - **同标题不同 id**:标题只是标签,`sessionId` 才是身份。两个会话可以标题完全相同但一个是已绑定、一个是未绑定;`sameTitleGroups` 会把它们放在同一组,给出 `bound` 数量、各自的 `libraries`(未绑定位置为 `null`)、`mergeable: false`、`identity: "sessionId"`,并且从不输出标题原文。诊断不会因为标题相同就合并、复制或删除绑定。
27
+ - **预设消失又出现**:`agents` 是活 agent 视图,只能证明“在场”,不能证明“缺席”。预设从列表消失时 `host.status` 变为 `partial`(或 `not-loaded`),`host-session-missing` 被扣住;绑定、作用域和待注入队列原样保留。预设回来时仍是同一条绑定,不产生重复。
28
+ - **删除会话**:睡眠整理报告会话已删除时,绑定保留并打上 `sessionGone`。这是“保留以便恢复”的标记,不是删除指令。若宿主会话注册表同时权威地不列出该 id,会额外出现 `host-session-missing`;它是两个独立事实,和 `pending-inbound` 可以同时存在。作用域、`libraryId`、待注入消息都不因删除标记而丢失。
29
+ - **恢复会话**:同一 `sessionId` 恢复后,`sessionGone` 应为 `false`,绑定继续使用原来的库、作用域和队列;诊断只观察这个状态,不负责清除标记,也不写回。恢复后的候选只剩仍然存在的独立事实(例如队列里的待注入消息)。
30
+ - **宿主查询失败**:`host` 缺失、`not-loaded`、`partial`、空权威列表、活 agent 列表或未知来源都不能推出“会话不存在”。报告保留全部绑定、`libraryId`、`memoryContext.scopeKeys`、`inbound.pending`,只增加 `host-unavailable`/`host-not-loaded`/`host-partial` 等未知项,绝不下发 `host-session-missing` 删除信号。
31
+ - **库指针异常**:`libraryId` 指向不存在的库时报告 `library-missing`,但不改写指针;库被归档时报告 `library-archived`,但归档库仍在磁盘上,不等于可以解绑。
32
+
33
+ ### 12.3 诊断字段
34
+
35
+ `diagnoseBindings()` 返回一个 JSON-safe 报告:
36
+
37
+ | 字段 | 含义 |
38
+ |---|---|
39
+ | `schema` | `dsh-memu-memory/binding-diagnostics@1` |
40
+ | `generatedAt` | 报告生成时刻(由 `now` 决定,便于测试) |
41
+ | `readOnly` / `mode` | 固定 `true` / `report-only` |
42
+ | `refusals` | 明确拒绝的动作:`gc`、`delete-binding`、`delete-session`、`delete-library`、`write-state` 等 |
43
+ | `thresholds` | `{ idleInspectionDays: 30, idleIsDeletionTrigger: false }` |
44
+ | `state` | 快照文件 `file`、`status`(ok/missing/corrupt/unreadable)、`bytes`、`mtimeMs`、`sha256` |
45
+ | `analysis` | 文件不可读时为 `null`;可读时为下面的分析对象 |
46
+ | `unknowns` | 不可观察或不可判定的项,带 `code`、`detail`、`keeps` |
47
+ | `readOnlyProof` | 读取前后的 sha256、字节数、mtime 对比和 `unchanged` |
48
+
49
+ `analysis.summary` 的计数:`bindings`、`host`(status/source/complete/sessions/unboundHostSessions)、`switches`、`libraries`、`memoryContext`、`guide`、`inbound`、`sleep`、`unfinished`、`runtime`(无进程内快照时为 `null`)、`candidates`、`sameTitleGroups`。
50
+
51
+ 每个 `analysis.bindings[]`(由 `bindingFacts` 产生)包含:`sessionId`、`switches`、`effective`、`folder`(仅 `scopeKeyHash`,不含路径)、`domain`、`library`、`memoryContext`(仅 `present` 和 `scopeKeys`)、`guide`、`inbound`、`sleep`、`recall`、`injection`、`activity`。`activity` 同时给出精确的 `idleMs` 和仅用于展示的 `idleDays`(四舍五入到 1 位小数)。默认不输出错误原文;`--redact` 时连 session id 也做伪名化,标题和文件夹路径从不输出。
52
+
53
+ 每个候选形如 `{ sessionId, kind, inspectionOnly: true, action: "inspect", reason, evidence }`。候选种类包括 `session-gone`、`host-session-missing`、`library-missing`、`library-archived`、`library-enabled-but-unbound`、`pending-inbound`、`sleep-retry`、`recall-degraded`、`reinject-pending`、`idle-inspection`。**候选是“请人工看一眼”的提示,不是删除队列。**
54
+
55
+ `analysis.runtime`:调用方提供进程内快照时为 `{ status: "provided", source, layers }`;未提供时为 `{ status: "unknown", reason, layers }`,其中 `layers` 仍列出 `RUNTIME_STATE` 声明的层,但不含任何进程内计数。
56
+
57
+ ### 12.4 30 天阈值:检查线,不是删除触发器
58
+
59
+ `INSPECTION_DAYS = 30`。只有 `activity.idleMs > 30 * 24 * 60 * 60 * 1000` 才产生 `idle-inspection` 候选;**恰好 30 天不报,超过 1 毫秒才报**。判定使用精确毫秒 `idleMs`,`idleDays` 只用于展示,不能反过来参与比较。
60
+
61
+ 这条线只回答“要不要人工检查”。它不会:
62
+
63
+ - 删除会话、绑定、记忆或库;
64
+ - 自动解绑或清空待注入队列;
65
+ - 重置 `sleepRetryAt`、`lastTurnAt` 或其他时间戳;
66
+ - 触发 GC、flush、mark-injected 或任何状态写入。
67
+
68
+ 报告的 `thresholds.idleIsDeletionTrigger` 固定为 `false`,`REFUSALS` 固定包含 `gc` 与各类删除动作。即使报告里出现 `idle-inspection`,也只是提示。
69
+
70
+ ### 12.5 自动 GC 必须另行确认
71
+
72
+ W9 的交付范围是只读诊断,**不包含自动 GC**。将来若要做自动回收,必须作为独立工作包另行获得用户确认,并至少满足:
73
+
74
+ 1. 宿主权威确认目标会话确实已删除,而不是仅凭标题、超时或本地快照推断;
75
+ 2. 有可恢复的备份,且备份本身不被同一次清理覆盖;
76
+ 3. 在事务中重读最新状态、重新校验绑定与队列,避免用过期快照删除新数据;
77
+ 4. 留下可审计记录,说明删了什么、依据是什么、如何恢复;
78
+ 5. 永不删除记忆库本身,永不丢失 `inbound.pending`,恢复会话时永不丢失作用域。
79
+
80
+ 在这些条件满足前,诊断报告只做候选标记;任何“清理”“解绑”“删库”都需要用户显式操作。
81
+
82
+ ### 12.6 CLI 与只读证据
83
+
84
+ ```bash
85
+ node scripts/diagnose-bindings.mjs --state <state.json> [--json] [--redact] [--now <ISO>]
86
+ ```
87
+
88
+ 退出码:
89
+
90
+ | 码 | 含义 |
91
+ |---|---|
92
+ | 0 | 成功生成报告,没有检查候选 |
93
+ | 1 | 成功生成报告,有检查候选(仍只是提示,没有改动任何东西) |
94
+ | 2 | 用法错误(未知参数、非法 `--now`) |
95
+ | 3 | 没有报告:state.json 缺失、损坏或不可读;没有创建目录、没有修复文件 |
96
+
97
+ CLI 只写 stdout/stderr。它不创建父目录,不创建缺失的 `state.json`,不隔离损坏文件,不更新时间戳;文本报告会打印 `readOnlyProof`。测试覆盖 0/1/2/3 四种退出码、缺失路径不建目录、已有文件字节与 mtime 不变。
98
+
99
+ `readBindingSnapshot()` 直接 `fs.readFileSync`,不调用会修复文件的 `loadState()`;`diagnoseBindings()` 在分析后再哈希一次并比较 mtime,形成 `readOnlyProof`。缺失或损坏时 `analysis` 为 `null`,报告只说明状态并保持现场。
@@ -125,7 +125,7 @@ npm run check # node --check + i18n 键对齐 + package.j
125
125
  - [ ] `injectedIds` 记的是**真正进入文本**的条目?(§8.4)
126
126
  - [ ] token 预算走 `lib/budget.js`,没有 `tokens * 3`?(§8.4)
127
127
  - [ ] `pending_review` 被挡在注入与召回之外?(§3.9)
128
- - [ ] 没有每步注入、动态记忆没进 system prompt?(§1.3、§1.4)
128
+ - [ ] 没有每步注入、记忆规则与动态记忆都没进 system prompt?
129
129
 
130
130
  **数据安全**
131
131
  - [ ] 读失败没被当成空数据(改名 `*.corrupt-<ts>` + 拒绝写入)?(§2.3 铁律一)
@@ -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 项断言全通过。
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