dsh-memu-memory 1.1.1 → 1.1.3

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 (50) hide show
  1. package/AGENTS.md +8 -5
  2. package/README.md +22 -5
  3. package/README.zh.md +21 -4
  4. package/docs/contributing.md +5 -3
  5. package/docs/env-and-commands.md +13 -0
  6. package/docs/host-api.md +35 -3
  7. package/docs/memory-semantics.md +41 -1
  8. package/docs/troubleshooting.md +395 -350
  9. package/lib/activity-log.js +190 -0
  10. package/lib/backup.js +1103 -0
  11. package/lib/binding-diagnostics.js +630 -0
  12. package/lib/client.js +703 -50
  13. package/lib/curator.js +81 -37
  14. package/lib/cycle.js +3 -0
  15. package/lib/embedding-catalog.js +763 -0
  16. package/lib/embedding-download.js +407 -0
  17. package/lib/embedding-runtime.js +710 -0
  18. package/lib/engine.js +2 -2
  19. package/lib/index.js +61 -4
  20. package/lib/inject.js +195 -49
  21. package/lib/local-embedding-entry.js +145 -0
  22. package/lib/local-embedding-manager.js +249 -0
  23. package/lib/local-embedding-protocol.js +173 -0
  24. package/lib/local-embedding-worker.js +652 -0
  25. package/lib/local-vector-service.js +283 -0
  26. package/lib/memory.js +385 -45
  27. package/lib/memu-config.js +10 -22
  28. package/lib/memu.js +53 -19
  29. package/lib/paths.js +24 -0
  30. package/lib/preset-scope.js +121 -0
  31. package/lib/private.js +139 -13
  32. package/lib/rpc.js +45 -4
  33. package/lib/state.js +109 -161
  34. package/lib/tools.js +9 -5
  35. package/lib/vector-migration.js +878 -0
  36. package/lib/vector-profile.js +941 -0
  37. package/package.json +3 -2
  38. package/scripts/acceptance-local.mjs +5 -4
  39. package/scripts/chain-matrix.mjs +527 -0
  40. package/scripts/check-all.mjs +19 -0
  41. package/scripts/check-doc-drift.mjs +353 -0
  42. package/scripts/diagnose-bindings.mjs +176 -0
  43. package/scripts/local-runtime-install.mjs +142 -0
  44. package/scripts/local-runtime-verify.mjs +56 -0
  45. package/scripts/local-vector-verify.mjs +93 -0
  46. package/scripts/recall-bench.mjs +813 -0
  47. package/scripts/repetition-ab-scenarios.mjs +93 -0
  48. package/scripts/repetition-detector-snapshot.mjs +314 -0
  49. package/scripts/repetition-eval-live.mjs +360 -0
  50. package/scripts/repetition-eval.mjs +702 -0
@@ -0,0 +1,190 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { logsDir, ensureDir, redactSecrets, warn } from './paths.js'
5
+
6
+ // 记忆链路的活动日志:召回、写入、整理、向量。用途是回答"刚才那一下到底走了哪条路、
7
+ // 召回/写入了什么、为什么没写成",而不是给开发者看堆栈。
8
+ //
9
+ // 三条硬约束:
10
+ // 1. 永不抛错。日志坏了不能把召回/写入一起弄坏(fail-open),失败只 warn 一次。
11
+ // 2. 永不无限增长。内存里保留最近 MAX_ENTRIES 条,文件超过 MAX_FILE_BYTES 时重写成
12
+ // 最近一半;这是本机诊断用的环形日志,不是审计材料。
13
+ // 3. 出盘前脱敏。message 与 detail 都过 redactSecrets;detail 只留诊断需要的字段,
14
+ // 正文最多 200 字符(召回查询、记忆标题),足够定位问题又不把整段对话抄进日志。
15
+
16
+ export const ACTIVITY_KINDS = Object.freeze(['recall', 'write', 'consolidate', 'curate', 'vector', 'error', 'system'])
17
+ const MAX_ENTRIES = 2000
18
+ const MAX_FILE_BYTES = 4 * 1024 * 1024
19
+ const MAX_MESSAGE_CHARS = 300
20
+ const MAX_DETAIL_CHARS = 4000
21
+ const MAX_QUERY_CHARS = 200
22
+
23
+ let cache = null
24
+ let loadWarned = false
25
+ let writeWarned = false
26
+
27
+ function activityFile() {
28
+ return path.join(logsDir(), 'activity.jsonl')
29
+ }
30
+
31
+ function clamp(value, max) {
32
+ const text = String(value ?? '')
33
+ return text.length > max ? text.slice(0, max - 1) + '…' : text
34
+ }
35
+
36
+ function safeDetail(value) {
37
+ if (value === undefined || value === null) return null
38
+ let text
39
+ try { text = JSON.stringify(value) } catch { text = JSON.stringify({ note: 'detail is not serializable' }) }
40
+ text = redactSecrets(text)
41
+ if (text.length > MAX_DETAIL_CHARS) text = text.slice(0, MAX_DETAIL_CHARS - 1) + '…'
42
+ try { return JSON.parse(text) } catch { return { text: text.slice(0, MAX_DETAIL_CHARS - 20) } }
43
+ }
44
+
45
+ function normalize(event = {}) {
46
+ const kind = ACTIVITY_KINDS.includes(event.kind) ? event.kind : 'system'
47
+ const level = ['info', 'warn', 'error'].includes(event.level) ? event.level : (event.ok === false ? 'warn' : 'info')
48
+ const at = typeof event.at === 'string' && !Number.isNaN(Date.parse(event.at)) ? event.at : new Date().toISOString()
49
+ const refs = Array.isArray(event.refs) ? event.refs.filter(item => typeof item === 'string' && item).slice(0, 20) : []
50
+ return {
51
+ id: 'act-' + Date.now().toString(36) + '-' + crypto.randomBytes(3).toString('hex'),
52
+ at,
53
+ kind,
54
+ level,
55
+ scope: event.scope === 'private' || event.scope === 'global' ? event.scope : null,
56
+ sessionId: typeof event.sessionId === 'string' ? event.sessionId : null,
57
+ libraryId: typeof event.libraryId === 'string' ? event.libraryId : null,
58
+ message: redactSecrets(clamp(event.message, MAX_MESSAGE_CHARS)),
59
+ detail: safeDetail(event.detail),
60
+ refs,
61
+ ok: event.ok !== false,
62
+ }
63
+ }
64
+
65
+ function parseLines(text) {
66
+ const out = []
67
+ for (const line of text.split('\n')) {
68
+ if (!line.trim()) continue
69
+ try {
70
+ const value = JSON.parse(line)
71
+ if (value && typeof value === 'object' && typeof value.at === 'string') out.push(value)
72
+ } catch {}
73
+ }
74
+ return out
75
+ }
76
+
77
+ function load() {
78
+ if (cache) return cache
79
+ cache = []
80
+ try {
81
+ const file = activityFile()
82
+ if (fs.existsSync(file)) cache = parseLines(fs.readFileSync(file, 'utf8')).slice(-MAX_ENTRIES)
83
+ } catch (error) {
84
+ if (!loadWarned) {
85
+ loadWarned = true
86
+ try { warn('activity log could not be read: ' + (error?.message || error) + ' (recall and writes continue; the log starts empty)') } catch {}
87
+ }
88
+ cache = []
89
+ }
90
+ return cache
91
+ }
92
+
93
+ function rewrite(entries) {
94
+ const file = activityFile()
95
+ ensureDir(path.dirname(file))
96
+ const temp = file + '.tmp-' + process.pid
97
+ fs.writeFileSync(temp, entries.map(entry => JSON.stringify(entry)).join('\n') + (entries.length ? '\n' : ''), 'utf8')
98
+ fs.renameSync(temp, file)
99
+ }
100
+
101
+ /** 记录一条活动。永不抛错;返回写入的条目(失败时返回 null)。 */
102
+ export function record(event) {
103
+ try {
104
+ const entry = normalize(event)
105
+ const entries = load()
106
+ entries.push(entry)
107
+ if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES)
108
+ try {
109
+ const file = activityFile()
110
+ ensureDir(path.dirname(file))
111
+ fs.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8')
112
+ let size = 0
113
+ try { size = fs.statSync(file).size } catch {}
114
+ if (size > MAX_FILE_BYTES) rewrite(entries.slice(-Math.floor(MAX_ENTRIES / 2)))
115
+ } catch (error) {
116
+ if (!writeWarned) {
117
+ writeWarned = true
118
+ try { warn('activity log could not be written: ' + (error?.message || error) + ' (memory operations are unaffected)') } catch {}
119
+ }
120
+ }
121
+ return entry
122
+ } catch {
123
+ return null
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 最近的活动,最新在前。`q` 只做字面包含匹配(大小写不敏感),不解释正则:
129
+ * 这是给人看的筛选框,不是查询语言。
130
+ */
131
+ export function listEvents({ limit = 200, kind = null, level = null, scope = null, sessionId = null, since = null, q = null } = {}) {
132
+ const wanted = ACTIVITY_KINDS.includes(kind) ? kind : null
133
+ const wantedLevel = ['info', 'warn', 'error'].includes(level) ? level : null
134
+ const sinceTime = typeof since === 'string' && !Number.isNaN(Date.parse(since)) ? Date.parse(since) : null
135
+ const needle = typeof q === 'string' && q.trim() ? q.trim().toLowerCase() : null
136
+ const count = Math.max(1, Math.min(MAX_ENTRIES, Number(limit) || 200))
137
+ const rows = []
138
+ const entries = load()
139
+ for (let index = entries.length - 1; index >= 0 && rows.length < count; index -= 1) {
140
+ const entry = entries[index]
141
+ if (wanted && entry.kind !== wanted) continue
142
+ if (wantedLevel && entry.level !== wantedLevel) continue
143
+ if (scope && entry.scope !== scope) continue
144
+ if (sessionId && entry.sessionId !== sessionId) continue
145
+ if (sinceTime && (Date.parse(entry.at) || 0) < sinceTime) continue
146
+ if (needle) {
147
+ const haystack = (entry.message + ' ' + JSON.stringify(entry.detail || {}) + ' ' + (entry.refs || []).join(' ')).toLowerCase()
148
+ if (!haystack.includes(needle)) continue
149
+ }
150
+ rows.push(entry)
151
+ }
152
+ return rows
153
+ }
154
+
155
+ export function logStats() {
156
+ const entries = load()
157
+ const counts = {}
158
+ for (const entry of entries) counts[entry.kind] = (counts[entry.kind] || 0) + 1
159
+ let bytes = 0
160
+ try { bytes = fs.statSync(activityFile()).size } catch {}
161
+ return { total: entries.length, bytes, counts, file: activityFile(), capped: MAX_ENTRIES, maxBytes: MAX_FILE_BYTES }
162
+ }
163
+
164
+ /** 清空日志(显式操作;不是自动裁剪)。 */
165
+ export function clearEvents() {
166
+ cache = []
167
+ try { rewrite([]) } catch (error) {
168
+ try { warn('activity log could not be cleared: ' + (error?.message || error)) } catch {}
169
+ }
170
+ return { ok: true }
171
+ }
172
+
173
+ /** 导出为可直接贴进 issue 的 JSONL 文本。 */
174
+ export function exportEvents(options = {}) {
175
+ return listEvents({ limit: Math.min(MAX_ENTRIES, Number(options.limit) || MAX_ENTRIES), ...options })
176
+ .map(entry => JSON.stringify(entry))
177
+ .join('\n')
178
+ }
179
+
180
+ /** 测试用:丢掉进程内缓存,下一次调用重新读盘。 */
181
+ export function resetActivityLogCache() {
182
+ cache = null
183
+ loadWarned = false
184
+ writeWarned = false
185
+ }
186
+
187
+ // 一个自动裁剪的便捷入口:`query` 是召回/整理里最常要看的东西,统一截断。
188
+ export function clipQuery(value) {
189
+ return clamp(redactSecrets(String(value ?? '')), MAX_QUERY_CHARS)
190
+ }