dsh-memu-memory 1.1.2 → 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 -6
- package/README.md +21 -5
- package/README.zh.md +21 -5
- 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 +32 -5
- package/docs/troubleshooting.md +36 -7
- package/lib/activity-log.js +190 -0
- package/lib/backup.js +1103 -0
- package/lib/binding-diagnostics.js +630 -0
- package/lib/client.js +697 -49
- 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 +36 -0
- package/lib/inject.js +131 -30
- 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/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,283 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { downloadsDir, warn } from './paths.js'
|
|
4
|
+
import { listLibraries, loadPrivateIndex, loadGlobalIndex } from './state.js'
|
|
5
|
+
import { commit as memuCommit, listFiles as memuListFiles, forget as memuForget } from './memu.js'
|
|
6
|
+
import { getEmbeddingCatalog } from './embedding-catalog.js'
|
|
7
|
+
import { createEmbeddingDownloadManager } from './embedding-download.js'
|
|
8
|
+
import { createVectorMigrator } from './vector-migration.js'
|
|
9
|
+
import { profileFromCatalog, setRequestedProfile, vectorStatus, assertProfileVerified } from './vector-profile.js'
|
|
10
|
+
import { embeddingWorkerManager } from './local-embedding-manager.js'
|
|
11
|
+
import { managedEmbeddingRuntime, resetManagedEmbeddingRuntime } from './embedding-runtime.js'
|
|
12
|
+
|
|
13
|
+
const root = path.join(downloadsDir(), 'embeddings')
|
|
14
|
+
// 受管 runtime 与模型制品分开放:模型在 downloads/embeddings/<id>,运行时在
|
|
15
|
+
// downloads/embeddings/runtime/<key>。切换/回滚模型永远不碰运行时目录。
|
|
16
|
+
const runtimeRoot = path.join(root, 'runtime')
|
|
17
|
+
let downloader
|
|
18
|
+
let migrator
|
|
19
|
+
let installer
|
|
20
|
+
|
|
21
|
+
function getDownloader() {
|
|
22
|
+
return downloader ||= createEmbeddingDownloadManager({ root })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// warn 走 paths.js 的 sink(index.js 装成 ctx.logger.warn):安装失败必须留可观测信号
|
|
26
|
+
// (§1 规则 10),但它绝不能把召回一起拖垮——召回仍然 fail-open 到词法。
|
|
27
|
+
function getInstaller() {
|
|
28
|
+
return installer ||= managedEmbeddingRuntime({ root: runtimeRoot, logger: { warn: message => warn(message) } })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function envRuntimeDir() {
|
|
32
|
+
const value = String(process.env.DSH_MEMU_EMBED_RUNTIME_DIR || '').trim()
|
|
33
|
+
return value ? path.resolve(value) : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 目录里真的有一套可 require 的运行时吗(不是「env 设了就算」)。 */
|
|
37
|
+
export function hasEmbeddingRuntimePackages(dir) {
|
|
38
|
+
if (typeof dir !== 'string' || !dir.trim()) return false
|
|
39
|
+
try {
|
|
40
|
+
return fs.statSync(path.join(path.resolve(dir.trim()), 'node_modules', 'onnxruntime-node', 'package.json')).isFile()
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 运行时状态 = 受管安装事实 + 用户 env 覆盖事实,两者都如实报告。
|
|
48
|
+
* env 指到一个没有运行时的目录时不会被当成 ready,而是回落到受管目录并留 warn。
|
|
49
|
+
*/
|
|
50
|
+
export function localVectorRuntimeStatus(options = {}) {
|
|
51
|
+
const env = envRuntimeDir()
|
|
52
|
+
const envUsable = env != null && hasEmbeddingRuntimePackages(env)
|
|
53
|
+
let status
|
|
54
|
+
try {
|
|
55
|
+
status = getInstaller().getStatus({ deep: options.deep === true, modelId: options.modelId || null })
|
|
56
|
+
} catch (error) {
|
|
57
|
+
status = { status: 'unsupported', ready: false, path: null, platform: null, key: null, error: { code: String(error?.code || 'LOCAL_VECTOR_RUNTIME_UNVERIFIED'), message: String(error?.message || error) } }
|
|
58
|
+
}
|
|
59
|
+
if (envUsable) return { ...status, status: 'configured', ready: true, source: 'env', path: env, managedStatus: status.status, error: null }
|
|
60
|
+
return { ...status, source: status.status === 'installed' ? 'managed' : null, ...(env ? { envIgnored: true } : {}) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 受管安装:只有 confirmed === true 才会联网下载 catalog 里 pin 住的制品。 */
|
|
64
|
+
export async function installLocalVectorRuntime(options = {}) {
|
|
65
|
+
const result = await getInstaller().ensure({
|
|
66
|
+
modelId: options.modelId || null,
|
|
67
|
+
confirmed: options.confirmed === true,
|
|
68
|
+
reuseOnly: options.reuseOnly === true,
|
|
69
|
+
signal: options.signal,
|
|
70
|
+
onProgress: options.onProgress,
|
|
71
|
+
})
|
|
72
|
+
return { runtime: result, state: getLocalVectorState() }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 生效的运行时目录:用户显式 env > 已核实的受管目录 > profile 里记录的旧值。 */
|
|
76
|
+
function runtimeDirFor(profile) {
|
|
77
|
+
const env = envRuntimeDir()
|
|
78
|
+
if (env && hasEmbeddingRuntimePackages(env)) return env
|
|
79
|
+
if (env) warn('DSH_MEMU_EMBED_RUNTIME_DIR does not contain the managed runtime packages; using the managed runtime instead')
|
|
80
|
+
const recorded = typeof profile?.runtimeDir === 'string' && profile.runtimeDir.trim() ? profile.runtimeDir.trim() : ''
|
|
81
|
+
if (recorded && hasEmbeddingRuntimePackages(recorded)) return path.resolve(recorded)
|
|
82
|
+
const status = getInstaller().getStatus({ modelId: profile?.catalogId || null })
|
|
83
|
+
return status.status === 'installed' ? status.path : null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function profileFor(catalogId) {
|
|
87
|
+
const id = String(catalogId)
|
|
88
|
+
return profileFromCatalog(id, { modelDir: path.join(root, id), runtimeDir: runtimeDirFor({ catalogId: id }) })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function installationFor(id, sharedRuntime = null) {
|
|
92
|
+
const status = getDownloader().getStatus(id, { deep: false })
|
|
93
|
+
// 所有可用模型 pin 的是同一个受管 runtime;每个模型各走一遍运行时目录树(几百个文件)
|
|
94
|
+
// 会让每次 getState 多花几百毫秒。上层已经算过一次就复用。
|
|
95
|
+
const runtime = sharedRuntime || localVectorRuntimeStatus({ modelId: id })
|
|
96
|
+
return {
|
|
97
|
+
status: status.state,
|
|
98
|
+
installed: status.installed === true,
|
|
99
|
+
modelDir: status.installed ? path.join(root, id) : null,
|
|
100
|
+
runtimeDir: runtime.ready ? runtime.path : null,
|
|
101
|
+
fingerprintMatches: status.fingerprintMatches ?? null,
|
|
102
|
+
error: status.error || null,
|
|
103
|
+
progress: status.progress || null,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* MemU `commit` 只认 `{ recall_files, resource, user }` 信封(写入与策划路径都走这个形状)。
|
|
109
|
+
* 迁移器原先直接把 `{ name, description, ...entry }` 裸对象传进去:MemU 返回成功却一行
|
|
110
|
+
* 都没写,随后回读不到 → 每条都 `VECTOR_READBACK_FAILED`,整次重建作废。
|
|
111
|
+
*/
|
|
112
|
+
export function migrationCommitPayload(item) {
|
|
113
|
+
const entry = item?.entry || {}
|
|
114
|
+
const text = String(entry.content ?? item?.text ?? '')
|
|
115
|
+
const title = String(entry.title ?? item?.text ?? item?.id ?? '')
|
|
116
|
+
return {
|
|
117
|
+
recall_files: [{ name: String(item?.id ?? ''), track: 'memory', description: title.slice(0, 180), content: text }],
|
|
118
|
+
resource: [],
|
|
119
|
+
user: {},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function migratorFor() {
|
|
124
|
+
return migrator ||= createVectorMigrator({
|
|
125
|
+
listLibraries: () => listLibraries().map(item => item.id),
|
|
126
|
+
loadIndex: libraryId => libraryId == null ? loadGlobalIndex() : loadPrivateIndex(libraryId),
|
|
127
|
+
commit: ({ db, item, profile }) => {
|
|
128
|
+
assertProfileVerified(profile)
|
|
129
|
+
return memuCommit({
|
|
130
|
+
db,
|
|
131
|
+
vectorProfile: profile,
|
|
132
|
+
route: 'managed-local',
|
|
133
|
+
payload: migrationCommitPayload(item),
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
forget: ({ db, itemId }) => memuForget({ db, name: itemId, route: 'managed-local' }),
|
|
137
|
+
listFiles: ({ db, profile }) => {
|
|
138
|
+
assertProfileVerified(profile)
|
|
139
|
+
return memuListFiles({ db, vectorProfile: profile, route: 'managed-local' })
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getLocalVectorState() {
|
|
145
|
+
const catalog = getEmbeddingCatalog().map(entry => ({
|
|
146
|
+
id: entry.id,
|
|
147
|
+
displayName: entry.displayName,
|
|
148
|
+
availability: entry.availability,
|
|
149
|
+
dimension: entry.dimension,
|
|
150
|
+
totalBytes: entry.totalBytes,
|
|
151
|
+
pendingReasons: [...(entry.pendingReasons || [])],
|
|
152
|
+
license: entry.license?.base || null,
|
|
153
|
+
}))
|
|
154
|
+
let indexed = null
|
|
155
|
+
try { indexed = migratorFor().membership() } catch { indexed = null }
|
|
156
|
+
const runtime = localVectorRuntimeStatus()
|
|
157
|
+
return {
|
|
158
|
+
catalog,
|
|
159
|
+
installations: Object.fromEntries(catalog.map(entry => [entry.id, installationFor(entry.id, runtime)])),
|
|
160
|
+
runtime,
|
|
161
|
+
vector: { ...vectorStatus(), indexed },
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function selectLocalVectorModel(id) {
|
|
166
|
+
const status = getDownloader().getStatus(String(id), { deep: true })
|
|
167
|
+
if (status.state !== 'installed' || status.fingerprintMatches === false) {
|
|
168
|
+
const error = new Error('install and verify the local embedding model before selecting it')
|
|
169
|
+
error.code = 'LOCAL_VECTOR_MODEL_NOT_INSTALLED'
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
setRequestedProfile(profileFor(id), { status: 'requested' })
|
|
173
|
+
return getLocalVectorState()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function downloadLocalVectorModel(id, options = {}) {
|
|
177
|
+
const modelId = String(id)
|
|
178
|
+
const result = await getDownloader().download(modelId, { confirmed: options.confirmed === true })
|
|
179
|
+
// Model and runtime downloads each require their own explicit confirmation.
|
|
180
|
+
return { result, runtime: null, state: getLocalVectorState() }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function switchLocalVectorProfile(options = {}) {
|
|
184
|
+
const state = vectorStatus()
|
|
185
|
+
const requested = state.requested?.catalogId || options.id
|
|
186
|
+
if (!requested) throw Object.assign(new Error('select a requested local embedding model first'), { code: 'LOCAL_VECTOR_REQUEST_MISSING' })
|
|
187
|
+
const profile = profileFor(requested)
|
|
188
|
+
if (!profile.runtimeDir) throw Object.assign(new Error('a verified managed runtime is required before switching local embeddings; install it from the managed vector settings first'), { code: 'LOCAL_VECTOR_RUNTIME_UNAVAILABLE' })
|
|
189
|
+
const result = await migratorFor().switchProfile({ profile })
|
|
190
|
+
return { result, state: getLocalVectorState() }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function rollbackLocalVectorProfile() {
|
|
194
|
+
const result = await migratorFor().rollbackProfile()
|
|
195
|
+
return { result, state: getLocalVectorState() }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function abortLocalVectorMigration() {
|
|
199
|
+
const result = migratorFor().abortPending()
|
|
200
|
+
return { result, state: getLocalVectorState() }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 启动 loopback worker 并真的打一次 POST /v1/embeddings:200 + 正确维度 + 有限数值才算就绪。
|
|
205
|
+
* 失败一律带 fallback: 'lexical',上游照旧 fail-open,但错误必须留在日志/lastRecallError 里。
|
|
206
|
+
*/
|
|
207
|
+
export async function healthCheckLocalVector(profile, { timeoutMs = 30_000 } = {}) {
|
|
208
|
+
const endpoint = await embeddingWorkerManager().acquire(profile)
|
|
209
|
+
const url = new URL('/v1/embeddings', endpoint.baseUrl).href
|
|
210
|
+
let response
|
|
211
|
+
try {
|
|
212
|
+
response = await fetch(url, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + String(endpoint.token) },
|
|
215
|
+
body: JSON.stringify({ model: endpoint.model, input: ['本地向量健康检查'] }),
|
|
216
|
+
signal: AbortSignal.timeout(Math.max(1000, Number(timeoutMs) || 30_000)),
|
|
217
|
+
})
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throw Object.assign(new Error('the local embedding worker did not answer /v1/embeddings: ' + String(error?.message || error)), { code: 'LOCAL_VECTOR_HEALTH_FAILED', fallback: 'lexical' })
|
|
220
|
+
}
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw Object.assign(new Error('the local embedding worker answered /v1/embeddings with ' + response.status), { code: 'LOCAL_VECTOR_HEALTH_FAILED', fallback: 'lexical', status: response.status })
|
|
223
|
+
}
|
|
224
|
+
const payload = await response.json().catch(() => null)
|
|
225
|
+
const vector = payload?.data?.[0]?.embedding
|
|
226
|
+
const dimension = Array.isArray(vector) ? vector.length : 0
|
|
227
|
+
if (!Array.isArray(vector) || dimension !== Number(profile.dimension)) {
|
|
228
|
+
throw Object.assign(new Error('the local embedding worker returned no ' + Number(profile.dimension) + '-dimension vector'), { code: 'LOCAL_VECTOR_HEALTH_FAILED', fallback: 'lexical', dimension })
|
|
229
|
+
}
|
|
230
|
+
if (!vector.every(value => typeof value === 'number' && Number.isFinite(value))) {
|
|
231
|
+
throw Object.assign(new Error('the local embedding worker returned non-finite vector components'), { code: 'LOCAL_VECTOR_HEALTH_FAILED', fallback: 'lexical' })
|
|
232
|
+
}
|
|
233
|
+
return { ok: true, path: '/v1/embeddings', status: response.status, model: endpoint.model, dimension }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 首用引导:runtime 复用(或确认后安装)→ 起 worker → /v1/embeddings 健康检查。
|
|
238
|
+
* 没有云端回退:任一环失败都是 typed error + fallback=lexical。
|
|
239
|
+
*/
|
|
240
|
+
export async function bootstrapLocalVector(options = {}) {
|
|
241
|
+
const state = vectorStatus()
|
|
242
|
+
const catalogId = options.id || state.active?.profile?.catalogId || state.requested?.catalogId || null
|
|
243
|
+
if (!catalogId) throw Object.assign(new Error('select a local embedding model before bootstrapping the local runtime'), { code: 'LOCAL_VECTOR_NOT_SELECTED' })
|
|
244
|
+
const before = localVectorRuntimeStatus({ modelId: catalogId })
|
|
245
|
+
let runtime
|
|
246
|
+
if (before.status === 'configured') {
|
|
247
|
+
runtime = { status: 'configured', ready: true, source: 'env', path: before.path, reused: true }
|
|
248
|
+
} else {
|
|
249
|
+
try {
|
|
250
|
+
runtime = await getInstaller().ensure({
|
|
251
|
+
modelId: catalogId,
|
|
252
|
+
confirmed: options.confirmed === true,
|
|
253
|
+
reuseOnly: options.confirmed !== true,
|
|
254
|
+
signal: options.signal,
|
|
255
|
+
onProgress: options.onProgress,
|
|
256
|
+
})
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// 引导失败照旧 fail-open 到词法:错误如实抛出,但必须带上降级标记。
|
|
259
|
+
if (error && typeof error === 'object' && error.fallback === undefined) error.fallback = 'lexical'
|
|
260
|
+
throw error
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const health = await healthCheckLocalVector(profileFor(catalogId), { timeoutMs: options.timeoutMs })
|
|
264
|
+
return { ok: true, runtime: { status: runtime.status, ready: true, path: runtime.path || null, key: runtime.key || null, source: runtime.source || 'managed', reused: runtime.reused === true }, model: health.model, dimension: health.dimension, health, state: getLocalVectorState() }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function testLocalVector() {
|
|
268
|
+
return bootstrapLocalVector({ confirmed: false })
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function closeLocalVectorService() {
|
|
272
|
+
try { getDownloader().dispose() } catch {}
|
|
273
|
+
try { getInstaller().abort() } catch {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** 仅供测试:清掉进程级单例,避免测试之间互相污染。 */
|
|
277
|
+
export function resetLocalVectorService() {
|
|
278
|
+
try { getDownloader().dispose() } catch {}
|
|
279
|
+
downloader = null
|
|
280
|
+
migrator = null
|
|
281
|
+
installer = null
|
|
282
|
+
resetManagedEmbeddingRuntime()
|
|
283
|
+
}
|