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.
- package/AGENTS.md +8 -5
- package/README.md +22 -5
- package/README.zh.md +21 -4
- package/docs/contributing.md +5 -3
- package/docs/env-and-commands.md +13 -0
- package/docs/host-api.md +35 -3
- package/docs/memory-semantics.md +41 -1
- package/docs/troubleshooting.md +395 -350
- package/lib/activity-log.js +190 -0
- package/lib/backup.js +1103 -0
- package/lib/binding-diagnostics.js +630 -0
- package/lib/client.js +703 -50
- package/lib/curator.js +81 -37
- package/lib/cycle.js +3 -0
- package/lib/embedding-catalog.js +763 -0
- package/lib/embedding-download.js +407 -0
- package/lib/embedding-runtime.js +710 -0
- package/lib/engine.js +2 -2
- package/lib/index.js +61 -4
- package/lib/inject.js +195 -49
- package/lib/local-embedding-entry.js +145 -0
- package/lib/local-embedding-manager.js +249 -0
- package/lib/local-embedding-protocol.js +173 -0
- package/lib/local-embedding-worker.js +652 -0
- package/lib/local-vector-service.js +283 -0
- package/lib/memory.js +385 -45
- package/lib/memu-config.js +10 -22
- package/lib/memu.js +53 -19
- package/lib/paths.js +24 -0
- package/lib/preset-scope.js +121 -0
- package/lib/private.js +139 -13
- package/lib/rpc.js +45 -4
- package/lib/state.js +109 -161
- package/lib/tools.js +9 -5
- package/lib/vector-migration.js +878 -0
- package/lib/vector-profile.js +941 -0
- package/package.json +3 -2
- package/scripts/acceptance-local.mjs +5 -4
- package/scripts/chain-matrix.mjs +527 -0
- package/scripts/check-all.mjs +19 -0
- package/scripts/check-doc-drift.mjs +353 -0
- package/scripts/diagnose-bindings.mjs +176 -0
- package/scripts/local-runtime-install.mjs +142 -0
- package/scripts/local-runtime-verify.mjs +56 -0
- package/scripts/local-vector-verify.mjs +93 -0
- package/scripts/recall-bench.mjs +813 -0
- package/scripts/repetition-ab-scenarios.mjs +93 -0
- package/scripts/repetition-detector-snapshot.mjs +314 -0
- package/scripts/repetition-eval-live.mjs +360 -0
- 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
|
+
}
|