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,165 @@
1
+ /**
2
+ * 主动记忆判定:一次闲时检查值不值得花 token。
3
+ *
4
+ * 三条输入都是"结构性"的,不做关键词判定:
5
+ * H 真人回合数(只算 source.kind === 'user' 的消息)
6
+ * C 新增证据量(真人文字 + 助手结论 + 去重后的工具结果,按字符估算)
7
+ * U 有效工具工作单元(去重的成功工具结果;轮询、空输出、重复错误只算一次)
8
+ *
9
+ * 触发规则(可被 settings 覆盖):
10
+ * - 会话空闲 < activeMemoryStarvationMinutes:分数必须 >= minScore;
11
+ * - 空闲 >= 饥饿阈值:小会话无条件检查(保证不会永远轮不到);
12
+ * - 大 transcript(>= largeTranscriptEvents)不吃饥饿豁免:真机回放里
13
+ * 5913 条事件的会话,一个"继续"回合只有 0.14~0.43 分,却会把整段
14
+ * 26MB 转录重新种进 fork;这种薄回合必须等分数达标;
15
+ * - 近期刚整理过 / 刚注入过记忆:直接抑制,避免"记忆后立刻再 fork 一次"。
16
+ *
17
+ * 纯函数,无 IO,便于回放标定与单测。
18
+ */
19
+
20
+ export const ACTIVE_MEMORY_VERSION = 1
21
+
22
+ export const ACTIVE_MEMORY_POLICY = Object.freeze({
23
+ version: ACTIVE_MEMORY_VERSION,
24
+ minScore: 0.8,
25
+ starvationMinutes: 15,
26
+ consolidatedCooldownMinutes: 60,
27
+ recallCooldownMinutes: 15,
28
+ humanTurnsCap: 3,
29
+ evidenceCharsCap: 4000,
30
+ toolUnitsCap: 20,
31
+ partCap: 2,
32
+ // 超过这个规模,fork 的种子成本随整段转录增长,饥饿不再单独构成理由。
33
+ largeTranscriptEvents: 2000,
34
+ // 大 transcript 的守卫只该推迟一次检查,不该无限推迟。待整理区间一旦超过这个规模,
35
+ // 无论分数多低都要整理一次:游标不前移,那段内容就一直躺在 pending 里等着,
36
+ // 「省 token」和「永远不记」之间的差别是延迟上界,不是覆盖上界。
37
+ // 600 事件大约相当于一次中等长度的会话;20k 事件的首轮整理本来就贵,但那是内容
38
+ // 本身的价格,不是规则造成的浪费。
39
+ maxPendingEvents: 600,
40
+ weights: Object.freeze({ human: 0.35, content: 0.45, tool: 0.2 }),
41
+ // 单个工具结果计入证据量的上限;整段工具日志不能挤掉真人说的话。
42
+ toolResultCharsCap: 2000,
43
+ })
44
+
45
+ function textOfContent(content) {
46
+ if (typeof content === 'string') return content.trim()
47
+ if (!Array.isArray(content)) return ''
48
+ return content
49
+ .filter(block => block && block.type === 'text' && typeof block.text === 'string')
50
+ .map(block => block.text)
51
+ .join(' ')
52
+ .trim()
53
+ }
54
+
55
+ /**
56
+ * 从一个事件窗口里收集证据。窗口应当只包含"上次检查之后"的事件;
57
+ * 调用方负责裁剪,这里不猜测边界。
58
+ */
59
+ export function collectEvidence(events, policy = ACTIVE_MEMORY_POLICY) {
60
+ const evidence = {
61
+ humanTurns: 0,
62
+ humanChars: 0,
63
+ assistantChars: 0,
64
+ toolChars: 0,
65
+ toolUnits: 0,
66
+ toolCalls: 0,
67
+ lastHumanText: '',
68
+ }
69
+ const seenUnits = new Set()
70
+ for (const event of events || []) {
71
+ const type = event?.type
72
+ if (type === 'user/message') {
73
+ if (event.data?.source?.kind !== 'user') continue
74
+ const text = textOfContent(event.data?.content)
75
+ if (!text) continue
76
+ evidence.humanTurns += 1
77
+ evidence.humanChars += text.length
78
+ evidence.lastHumanText = text
79
+ continue
80
+ }
81
+ if (type === 'assistant/message') {
82
+ const blocks = event.data?.message?.content
83
+ if (!Array.isArray(blocks)) continue
84
+ for (const block of blocks) {
85
+ if (block?.type === 'tool-call') evidence.toolCalls += 1
86
+ else if (block?.type === 'text' && typeof block.text === 'string') evidence.assistantChars += block.text.trim().length
87
+ }
88
+ continue
89
+ }
90
+ if (type === 'tool/call') {
91
+ evidence.toolCalls += 1
92
+ continue
93
+ }
94
+ if (type === 'tool/result') {
95
+ const message = event.data?.message || {}
96
+ if (message.isError === true) continue
97
+ const text = textOfContent(message.content)
98
+ if (!text) continue
99
+ // 同一份结果重复出现(轮询、重读、重试)只算一次工作单元。
100
+ const key = text.slice(0, 160) + '|' + text.length
101
+ if (seenUnits.has(key)) continue
102
+ seenUnits.add(key)
103
+ evidence.toolUnits += 1
104
+ evidence.toolChars += Math.min(text.length, policy.toolResultCharsCap)
105
+ }
106
+ }
107
+ return evidence
108
+ }
109
+
110
+ /** 可解释评分:每个分项封顶,避免"只刷工具次数"把分数拉满。 */
111
+ export function scoreEvidence(evidence, policy = ACTIVE_MEMORY_POLICY) {
112
+ const human = Number(evidence?.humanTurns) || 0
113
+ const contentChars = (Number(evidence?.humanChars) || 0) + (Number(evidence?.assistantChars) || 0) + (Number(evidence?.toolChars) || 0)
114
+ const units = Number(evidence?.toolUnits) || 0
115
+ const h = Math.min(human / policy.humanTurnsCap, policy.partCap)
116
+ const c = Math.min(contentChars / policy.evidenceCharsCap, policy.partCap)
117
+ const u = Math.min(units / policy.toolUnitsCap, policy.partCap)
118
+ const score = policy.weights.human * h + policy.weights.content * c + policy.weights.tool * u
119
+ return {
120
+ score: Math.round(score * 1000) / 1000,
121
+ parts: { human: h, content: c, tool: u, humanTurns: human, contentChars, toolUnits: units },
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 是否唤醒检查。真人回合是硬门槛(纯工具轮询不构成检查理由),
127
+ * 其余交给分数;`starved` 由调用方按空闲时长传入。
128
+ * `transcriptEvents` 是整段会话的事件数,用来决定饥饿豁免是否还有成本意义。
129
+ */
130
+ export function assessWake({ evidence, starved = false, transcriptEvents = 0, pendingEvents = 0, policy = ACTIVE_MEMORY_POLICY } = {}) {
131
+ const scored = scoreEvidence(evidence, policy)
132
+ if ((Number(evidence?.humanTurns) || 0) === 0) {
133
+ return { wake: false, reason: 'no-human-turn', ...scored }
134
+ }
135
+ // 积压优先于守卫:守卫的理由是「为一句轻话重种整段转录不划算」,但当整段转录本身
136
+ // 还没整理过时,那不是重种,是欠着。没有这条,一个大 transcript 只要每轮都是轻话,
137
+ // 就永远等不到检查——延迟没有上界,长会话的早期决定再也进不了记忆。
138
+ if (Number(pendingEvents) >= Number(policy.maxPendingEvents || Infinity)) {
139
+ return { wake: true, reason: 'backlog', ...scored }
140
+ }
141
+ const large = (Number(transcriptEvents) || 0) >= policy.largeTranscriptEvents
142
+ if (starved && !large) return { wake: true, reason: 'starved', ...scored }
143
+ if (scored.score < policy.minScore) {
144
+ return { wake: false, reason: large && starved ? 'large-transcript-thin' : 'low-score', ...scored }
145
+ }
146
+ return { wake: true, reason: 'score', ...scored }
147
+ }
148
+
149
+ /**
150
+ * 抑制原因:近期整理过(避免记忆后马上再 fork)或近期注入过记忆。
151
+ * 返回 null 表示不抑制。
152
+ */
153
+ export function suppressionReason({
154
+ now = Date.now(),
155
+ lastSleepAt = null,
156
+ lastInjectedAt = null,
157
+ consolidatedCooldownMinutes = ACTIVE_MEMORY_POLICY.consolidatedCooldownMinutes,
158
+ recallCooldownMinutes = ACTIVE_MEMORY_POLICY.recallCooldownMinutes,
159
+ } = {}) {
160
+ const sleep = Date.parse(lastSleepAt || '') || 0
161
+ if (sleep && now - sleep < consolidatedCooldownMinutes * 60_000) return 'recent-consolidation'
162
+ const recalled = Date.parse(lastInjectedAt || '') || 0
163
+ if (recalled && now - recalled < recallCooldownMinutes * 60_000) return 'recent-recall'
164
+ return null
165
+ }
@@ -13,7 +13,7 @@ import { logsDir, ensureDir, redactSecrets, warn } from './paths.js'
13
13
  // 3. 出盘前脱敏。message 与 detail 都过 redactSecrets;detail 只留诊断需要的字段,
14
14
  // 正文最多 200 字符(召回查询、记忆标题),足够定位问题又不把整段对话抄进日志。
15
15
 
16
- export const ACTIVITY_KINDS = Object.freeze(['recall', 'write', 'consolidate', 'curate', 'vector', 'error', 'system'])
16
+ export const ACTIVITY_KINDS = Object.freeze(['recall', 'write', 'consolidate', 'curate', 'vector', 'error', 'system', 'check'])
17
17
  const MAX_ENTRIES = 2000
18
18
  const MAX_FILE_BYTES = 4 * 1024 * 1024
19
19
  const MAX_MESSAGE_CHARS = 300
@@ -54,9 +54,9 @@ export const RUNTIME_STATE = Object.freeze([
54
54
  { layer: 'temporary-map', file: 'lib/inbound.js:13', name: 'calls', keyedBy: 'sessionId', clearedBy: 'tool/result or turn/end' },
55
55
  { layer: 'temporary-map', file: 'lib/inbound.js:14', name: 'sessions', keyedBy: 'sessionId', clearedBy: 'session/disposed (releaseInboundSession)' },
56
56
  { layer: 'temporary-map', file: 'lib/media.js:19', name: 'captures', keyedBy: 'sessionId', clearedBy: 'capture settle' },
57
- { layer: 'temporary-map', file: 'lib/private.js:265', name: 'inFlight', keyedBy: 'sessionId', clearedBy: 'consolidation settle' },
57
+ { layer: 'temporary-map', file: 'lib/private.js:267', name: 'inFlight', keyedBy: 'sessionId', clearedBy: 'consolidation settle' },
58
58
  { layer: 'temporary-map', file: 'lib/curator.js:13', name: 'jobs', keyedBy: 'job id', clearedBy: 'curation settle' },
59
- { layer: 'cache', file: 'lib/state.js:950', name: 'indexCache', keyedBy: 'libraryId', clearedBy: 'next load / external write' },
59
+ { layer: 'cache', file: 'lib/state.js:967', name: 'indexCache', keyedBy: 'libraryId', clearedBy: 'next load / external write' },
60
60
  { layer: 'cache', file: 'lib/paths.js:52', name: 'readFailures', keyedBy: 'absolute file', clearedBy: 'a later successful read' },
61
61
  ])
62
62
 
@@ -216,7 +216,12 @@ export function bindingFacts(sessionId, binding, { libraries = {}, policies = {}
216
216
  present: Boolean(context),
217
217
  scopeKeys: context ? SCOPE_KEYS.filter(key => typeof context[key] === 'string' && context[key]) : [],
218
218
  },
219
- guide: { sent: Boolean(b.guideMessageAt), sentAt: isoAt(b.guideMessageAt) },
219
+ guide: {
220
+ sent: Boolean(b.guideMessageAt),
221
+ sentAt: isoAt(b.guideMessageAt),
222
+ rulesKey: typeof b.guideRulesKey === 'string' ? b.guideRulesKey : null,
223
+ lost: b.guideLost === true,
224
+ },
220
225
  inbound: {
221
226
  pending: pending.length,
222
227
  pendingBytes,
package/lib/client.js CHANGED
@@ -131,6 +131,7 @@ window.__ModuleLoader__.load({
131
131
  logKindVector: '向量',
132
132
  logKindError: '错误',
133
133
  logKindSystem: '系统',
134
+ logKindCheck: '主动检查',
134
135
  logClear: '清空日志',
135
136
  runLogTitle: '整理记录',
136
137
  runLogEmpty: '还没有整理记录。',
@@ -637,6 +638,7 @@ window.__ModuleLoader__.load({
637
638
  logKindVector: 'Vector',
638
639
  logKindError: 'Error',
639
640
  logKindSystem: 'System',
641
+ logKindCheck: 'Check',
640
642
  logClear: 'Clear log',
641
643
  runLogTitle: 'Runs',
642
644
  runLogEmpty: 'No runs recorded yet.',
@@ -1765,6 +1767,20 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
1765
1767
  return enumText(t, DOMAIN_LABELS, raw)
1766
1768
  }
1767
1769
 
1770
+ // 情绪构成的短标签:同样的 valence 可能来自怒,也可能来自哀,一个数字分不出来。
1771
+ // 四轴固定且术语极短,就地映射,不为四个轴各加一个 i18n key。
1772
+ const EMOTION_TAG_LABELS = { joy: '喜', anger: '怒', sorrow: '哀', fear: '惧' }
1773
+ function emotionTagText(emotion) {
1774
+ if (!emotion || typeof emotion !== 'object') return ''
1775
+ return Object.entries(EMOTION_TAG_LABELS)
1776
+ .map(([axis, label]) => ({ label, share: Number(emotion[axis]) || 0 }))
1777
+ .filter((item) => item.share >= 0.05)
1778
+ .sort((a, b) => b.share - a.share)
1779
+ .slice(0, 2)
1780
+ .map((item) => `${item.label}${item.share.toFixed(1)}`)
1781
+ .join('/')
1782
+ }
1783
+
1768
1784
  // Tags are labels, not decoration — except when the label is a warning. A memory
1769
1785
  // marked "never say out loud" has to look different from one that can be said,
1770
1786
  // otherwise the badge is just more grey text to skim past.
@@ -3774,18 +3790,22 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
3774
3790
  h(Field, { label: t('autoInject') },
3775
3791
  h('select', { style: S.input, value: settings.autoInject || 'smart', onChange: (e) => setSetting('autoInject', e.target.value) },
3776
3792
  ['smart', 'anchor-only', 'per-turn', 'off'].map((v) => h('option', { key: v, value: v }, enumText(t, INJECT_LABELS, v))))),
3777
- h(Field, { label: t('privateTopK') }, h('input', { type: 'number', style: S.input, value: settings.privateTopK ?? 4, onChange: (e) => setSetting('privateTopK', Number(e.target.value)) })),
3778
- h(Field, { label: t('globalTopK') }, h('input', { type: 'number', style: S.input, value: settings.globalTopK ?? 3, onChange: (e) => setSetting('globalTopK', Number(e.target.value)) })),
3779
- h(Field, { label: t('mustRememberCap') }, h('input', { type: 'number', style: S.input, value: settings.mustRememberCap ?? 16, onChange: (e) => setSetting('mustRememberCap', Number(e.target.value)) })),
3793
+ // 下面这些 ?? 回落值必须与 lib/state.js 的 DEFAULT_SETTINGS 一致:浏览器 bundle 不能
3794
+ // import 服务端模块,这里是客户端唯一的一份副本。它只在首次 getState 回来之前显示,
3795
+ // 但用户恰好在这段窗口里点「保存」就会把回落值持久化,等于悄悄改掉默认值。
3796
+ // 一致性由 tests/ui-defaults.test.mjs 守着。
3797
+ h(Field, { label: t('privateTopK') }, h('input', { type: 'number', style: S.input, value: settings.privateTopK ?? 5, onChange: (e) => setSetting('privateTopK', Number(e.target.value)) })),
3798
+ h(Field, { label: t('globalTopK') }, h('input', { type: 'number', style: S.input, value: settings.globalTopK ?? 5, onChange: (e) => setSetting('globalTopK', Number(e.target.value)) })),
3799
+ h(Field, { label: t('mustRememberCap') }, h('input', { type: 'number', style: S.input, value: settings.mustRememberCap ?? 12, onChange: (e) => setSetting('mustRememberCap', Number(e.target.value)) })),
3780
3800
  h(Field, { label: t('shortTermTtlHours') }, h('input', { type: 'number', min: 0, style: S.input, value: settings.shortTermTtlHours ?? 72, onChange: (e) => setSetting('shortTermTtlHours', Number(e.target.value)) })),
3781
3801
  h(Field, { label: t('anchorTokens') }, h('input', { type: 'number', style: S.input, value: settings.anchorTokens ?? 800, onChange: (e) => setSetting('anchorTokens', Number(e.target.value)) })),
3782
3802
  h(Field, { label: t('turnTokens') }, h('input', { type: 'number', style: S.input, value: settings.turnTokens ?? 800, onChange: (e) => setSetting('turnTokens', Number(e.target.value)) })),
3783
3803
  h(Field, { label: t('injectionTotalTokens') }, h('input', { type: 'number', style: S.input, value: settings.injectionTotalTokens ?? 2000, onChange: (e) => setSetting('injectionTotalTokens', Number(e.target.value)) })),
3784
- h(Field, { label: t('dedupeTurns') }, h('input', { type: 'number', style: S.input, value: settings.dedupeTurns ?? 8, onChange: (e) => setSetting('dedupeTurns', Number(e.target.value)) })),
3785
- h(Field, { label: t('globalEveryNTurns') }, h('input', { type: 'number', style: S.input, value: settings.globalEveryNTurns ?? 3, onChange: (e) => setSetting('globalEveryNTurns', Number(e.target.value)) })),
3804
+ h(Field, { label: t('dedupeTurns') }, h('input', { type: 'number', style: S.input, value: settings.dedupeTurns ?? 16, onChange: (e) => setSetting('dedupeTurns', Number(e.target.value)) })),
3805
+ h(Field, { label: t('globalEveryNTurns') }, h('input', { type: 'number', style: S.input, value: settings.globalEveryNTurns ?? 6, onChange: (e) => setSetting('globalEveryNTurns', Number(e.target.value)) })),
3786
3806
  h(Field, { label: t('minUserChars') }, h('input', { type: 'number', style: S.input, value: settings.minUserChars ?? 6, onChange: (e) => setSetting('minUserChars', Number(e.target.value)) })),
3787
3807
  h(Field, { label: t('relevanceThreshold') }, h('input', { type: 'number', step: '0.05', min: 0, max: 1, style: S.input, value: settings.relevanceThreshold ?? 0.4, onChange: (e) => setSetting('relevanceThreshold', Number(e.target.value)) })),
3788
- h(Field, { label: t('randomCueChance') }, h('input', { type: 'number', step: '0.01', min: 0, max: 1, style: S.input, value: settings.randomCueChance ?? 0.05, onChange: (e) => setSetting('randomCueChance', Number(e.target.value)) })),
3808
+ h(Field, { label: t('randomCueChance') }, h('input', { type: 'number', step: '0.01', min: 0, max: 1, style: S.input, value: settings.randomCueChance ?? 0.02, onChange: (e) => setSetting('randomCueChance', Number(e.target.value)) })),
3789
3809
  h(Field, { label: t('autoInstall') }, h('input', { type: 'checkbox', checked: settings.autoInstallEngine !== false, onChange: (e) => setSetting('autoInstallEngine', e.target.checked) })),
3790
3810
  h(Field, { label: t('newSessionPrompt') }, h('input', { type: 'checkbox', checked: settings.newSessionPromptDismissed !== true, onChange: (e) => setSetting('newSessionPromptDismissed', !e.target.checked) })),
3791
3811
  h(Field, { label: t('habitsInjection') }, h('input', { type: 'checkbox', checked: settings.habitsInjection !== false, onChange: (e) => setSetting('habitsInjection', e.target.checked) })),
@@ -3795,7 +3815,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
3795
3815
  names: settings.inboundToolNames || [], onChange: values => setSetting('inboundToolNames', values),
3796
3816
  })),
3797
3817
  h(Field, { label: t('sleepConsolidation') }, h('input', { type: 'checkbox', checked: settings.sleepConsolidation === true, onChange: (e) => setSetting('sleepConsolidation', e.target.checked) })),
3798
- h(Field, { label: t('sleepAfterIdleMinutes') }, h('input', { type: 'number', min: 1, style: S.input, value: settings.sleepAfterIdleMinutes ?? 20, onChange: (e) => setSetting('sleepAfterIdleMinutes', Number(e.target.value)) })),
3818
+ h(Field, { label: t('sleepAfterIdleMinutes') }, h('input', { type: 'number', min: 1, style: S.input, value: settings.sleepAfterIdleMinutes ?? 3, onChange: (e) => setSetting('sleepAfterIdleMinutes', Number(e.target.value)) })),
3799
3819
  h(Field, { label: t('autoCurateEveryHours') }, h('input', { type: 'number', min: 0, style: S.input, value: settings.autoCurateEveryHours ?? 0, onChange: (e) => setSetting('autoCurateEveryHours', Number(e.target.value)) })),
3800
3820
  h(Field, { label: t('autoCurateSessionLimit') }, h('input', { type: 'number', min: 1, max: 30, style: S.input, value: settings.autoCurateSessionLimit ?? 8, onChange: (e) => setSetting('autoCurateSessionLimit', Number(e.target.value)) })),
3801
3821
  h(Field, { label: t('purgeArchivedAfterDays') }, h('input', { type: 'number', min: 0, style: S.input, value: settings.purgeArchivedAfterDays ?? 0, onChange: (e) => setSetting('purgeArchivedAfterDays', Number(e.target.value)) })),
@@ -4023,6 +4043,9 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
4023
4043
  const set = (key, value) => setBrowseDraft((prev) => ({
4024
4044
  ...prev, [f.id]: { ...(prev[f.id] || {}), [key]: value },
4025
4045
  }))
4046
+ // 有构成就讲构成;没有就回到旧的单值标签,老条目照旧可读。
4047
+ const moodText = emotionTagText(f.emotion)
4048
+ || (typeof f.moodValence === 'number' && f.moodValence !== 0 ? f.moodValence.toFixed(2) : '')
4026
4049
  return h('div', { key: f.id, style: S.item },
4027
4050
  h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center' } },
4028
4051
  h('div', { style: S.row },
@@ -4051,8 +4074,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
4051
4074
  f.salientDate
4052
4075
  ? h(Tag, null, `${/^\d{4}-\d{2}-\d{2}$/.test(f.salientDate) ? t('dueDateTag') : t('anniversaryTag')} ${f.salientDate}`)
4053
4076
  : null,
4054
- typeof f.moodValence === 'number' && f.moodValence !== 0
4055
- ? h(Tag, null, `${t('moodTag')} ${f.moodValence.toFixed(2)}`) : null,
4077
+ moodText ? h(Tag, null, `${t('moodTag')} ${moodText}`) : null,
4056
4078
  typeof f.accuracy === 'number' && typeof f.confidence === 'number' && f.accuracy <= 0.6 && f.accuracy < f.confidence - 0.1
4057
4079
  ? h(Tag, { tone: 'warn' }, `${t('accuracyTag')} ${f.accuracy}`) : null,
4058
4080
  h(Tag, null, enumText(t, STATUS_LABELS, f.status || 'active'))),
@@ -4442,7 +4464,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
4442
4464
 
4443
4465
  const LOG_KIND_LABELS = {
4444
4466
  recall: t('logKindRecall'), write: t('logKindWrite'), consolidate: t('logKindConsolidate'),
4445
- curate: t('logKindCurate'), vector: t('logKindVector'), error: t('logKindError'), system: t('logKindSystem'),
4467
+ curate: t('logKindCurate'), vector: t('logKindVector'), error: t('logKindError'), system: t('logKindSystem'), check: t('logKindCheck'),
4446
4468
  }
4447
4469
  const logsBlock = h(React.Fragment, null,
4448
4470
  h(Section, {
@@ -4472,7 +4494,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
4472
4494
  onChange: (event) => setLogKind(event.target.value),
4473
4495
  },
4474
4496
  h('option', { value: '' }, t('logAllKinds')),
4475
- ['recall', 'write', 'consolidate', 'curate', 'vector', 'error'].map(kind => h('option', { key: kind, value: kind }, LOG_KIND_LABELS[kind]))),
4497
+ ['recall', 'write', 'consolidate', 'curate', 'vector', 'error', 'check'].map(kind => h('option', { key: kind, value: kind }, LOG_KIND_LABELS[kind]))),
4476
4498
  h('input', {
4477
4499
  style: S.input,
4478
4500
  value: logQueryInput,