dsh-prime-memory 0.26.29 → 0.26.30
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/CHANGELOG.md +18 -0
- package/README.de.md +2 -0
- package/README.en.md +2 -0
- package/README.es.md +2 -0
- package/README.fr.md +2 -0
- package/README.it.md +2 -0
- package/README.ja.md +2 -0
- package/README.ko.md +2 -0
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/dist/client.js +1389 -1274
- package/dist/clients/drsc-inproc.d.ts +34 -0
- package/dist/clients/drsc-inproc.js +88 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +8 -0
- package/dist/contract.d.ts +16 -0
- package/dist/embedding-child.cjs +18 -2
- package/dist/embedding-worker.cjs +10 -3
- package/dist/index.js +7 -1
- package/dist/pipeline/intra-dedupe.d.ts +85 -0
- package/dist/pipeline/intra-dedupe.js +224 -0
- package/dist/pipeline/l1.js +47 -10
- package/dist/stats.js +7 -0
- package/dist/store/embedding-source.d.ts +3 -1
- package/dist/store/embedding-source.js +5 -2
- package/dist/store/l1.d.ts +28 -5
- package/dist/store/l1.js +45 -10
- package/dist/store/local-embedding.d.ts +50 -3
- package/dist/store/local-embedding.js +232 -13
- package/dist/store/sqlite.d.ts +8 -0
- package/dist/store/sqlite.js +17 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - 懒加载:首次嵌入/warmup 才让子进程加载模型,close()=kill 释放进程与
|
|
11
11
|
* 模型(嵌入源切走/关闭时调用),terminated 后不可复用;
|
|
12
|
+
* - 崩溃有界自愈(#22 机理刀,config `embedding.respawnOnCrash` 默认开):子进程被硬杀后
|
|
13
|
+
* 由 crashCb 单点重 fork+重预热,60s 窗内 ≤2 次;多条目批按实时断点(ack)续试一次,
|
|
14
|
+
* 不整批重放——毒输入最多多吃两批,不成活锁;超限回 failed(旧语义:换源/重启恢复);
|
|
12
15
|
* - 模型从数据目录 models/<id>/ 本地加载(子进程侧 env.allowRemoteModels=false);
|
|
13
16
|
* - channel 可注入(测试缝):用假通道验证协议与状态机,不触真模型;
|
|
14
17
|
* - useWorker=true 切回旧 worker_threads 通道(回滚逃生门;若宿主侧 DLL 冲突
|
|
@@ -23,6 +26,13 @@ import * as path from 'node:path';
|
|
|
23
26
|
import { fileURLToPath } from 'node:url';
|
|
24
27
|
import { Worker } from 'node:worker_threads';
|
|
25
28
|
import { deriveTokenBudget, HARD_CAP_TOKENS } from './model-catalog.js';
|
|
29
|
+
/**
|
|
30
|
+
* 崩溃自愈速率窗(#22 机理刀):60s 滚动窗内最多 2 次重 fork。
|
|
31
|
+
* 对齐 EmbedHelper 的 COOLDOWN_MS=60_000 口径;超限回 failed 是故意留的终局——
|
|
32
|
+
* 反复 fork 风暴比停摆更贵(每次 fork+载入≈1.7s、峰值≈550MB)。
|
|
33
|
+
*/
|
|
34
|
+
const RESPAWN_WINDOW_MS = 60_000;
|
|
35
|
+
const RESPAWN_MAX_PER_WINDOW = 2;
|
|
26
36
|
/** 默认 worker 资产路径(旧通道,useWorker 逃生门):dist/store/ → dist/embedding-worker.cjs(构建期拷入)。 */
|
|
27
37
|
function defaultWorkerPath() {
|
|
28
38
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'embedding-worker.cjs');
|
|
@@ -35,6 +45,8 @@ function defaultChildPath() {
|
|
|
35
45
|
class RealWorkerChannel {
|
|
36
46
|
worker;
|
|
37
47
|
pending = new Map();
|
|
48
|
+
/** #22 断点位:多条目批实时上报的 done 计数(按请求 id;回复/终止时清理)。 */
|
|
49
|
+
progress = new Map();
|
|
38
50
|
nextId = 1;
|
|
39
51
|
terminated = false;
|
|
40
52
|
crashed;
|
|
@@ -46,6 +58,14 @@ class RealWorkerChannel {
|
|
|
46
58
|
this.failAll(`本地嵌入 worker 致命错误: ${msg.error ?? '未知'}`);
|
|
47
59
|
return;
|
|
48
60
|
}
|
|
61
|
+
if (msg && msg.type === 'embedProgress') {
|
|
62
|
+
// 非请求-应答消息:按 cid 记断点位,不进 pending 配对(#22 机理刀)
|
|
63
|
+
const cid = msg.cid;
|
|
64
|
+
const done = msg.done;
|
|
65
|
+
if (typeof cid === 'number' && typeof done === 'number')
|
|
66
|
+
this.progress.set(cid, done);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
49
69
|
const id = msg.id;
|
|
50
70
|
if (typeof id !== 'number')
|
|
51
71
|
return;
|
|
@@ -77,6 +97,13 @@ class RealWorkerChannel {
|
|
|
77
97
|
this.worker.postMessage({ ...call, id });
|
|
78
98
|
});
|
|
79
99
|
}
|
|
100
|
+
isAlive() {
|
|
101
|
+
return !this.terminated && !this.crashed;
|
|
102
|
+
}
|
|
103
|
+
/** #22 断点续试:按 cid 读最后实时进度(旧通道死后的值保留在此供服务侧读)。 */
|
|
104
|
+
lastProgressFor(cid) {
|
|
105
|
+
return this.progress.get(cid);
|
|
106
|
+
}
|
|
80
107
|
terminate() {
|
|
81
108
|
if (this.terminated)
|
|
82
109
|
return;
|
|
@@ -94,6 +121,7 @@ class RealWorkerChannel {
|
|
|
94
121
|
for (const [, entry] of this.pending)
|
|
95
122
|
entry.reject(new Error(error));
|
|
96
123
|
this.pending.clear();
|
|
124
|
+
// progress **不清**:崩溃时刻的断点位正是服务侧要读的(lastProgressFor);cid 单调不复用,无跨批误读之虞
|
|
97
125
|
if (!this.terminated && !this.crashed) {
|
|
98
126
|
this.crashed = error;
|
|
99
127
|
this.crashCb?.(error);
|
|
@@ -129,6 +157,8 @@ export function describeEmbedBatch(modelId, n, tokensMax, ms) {
|
|
|
129
157
|
class RealChildChannel {
|
|
130
158
|
child;
|
|
131
159
|
pending = new Map();
|
|
160
|
+
/** #22 断点位:与 worker 通道同语义(多条目批实时 done 计数;failAll 不清,新批才清)。 */
|
|
161
|
+
progress = new Map();
|
|
132
162
|
nextId = 1;
|
|
133
163
|
terminated = false;
|
|
134
164
|
crashed;
|
|
@@ -148,6 +178,14 @@ class RealChildChannel {
|
|
|
148
178
|
this.failAll(`本地嵌入子进程致命错误: ${msg.error ?? '未知'}`);
|
|
149
179
|
return;
|
|
150
180
|
}
|
|
181
|
+
if (msg && msg.type === 'embedProgress') {
|
|
182
|
+
// 非请求-应答消息:按 cid 记断点位,不进 pending 配对(#22 机理刀;与 worker 通道同语义)
|
|
183
|
+
const cid = msg.cid;
|
|
184
|
+
const done = msg.done;
|
|
185
|
+
if (typeof cid === 'number' && typeof done === 'number')
|
|
186
|
+
this.progress.set(cid, done);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
151
189
|
const id = msg.id;
|
|
152
190
|
if (typeof id !== 'number')
|
|
153
191
|
return;
|
|
@@ -191,6 +229,14 @@ class RealChildChannel {
|
|
|
191
229
|
});
|
|
192
230
|
});
|
|
193
231
|
}
|
|
232
|
+
isAlive() {
|
|
233
|
+
return !this.terminated && !this.crashed;
|
|
234
|
+
}
|
|
235
|
+
/** #22 断点续试:按 cid 读最后实时进度(旧通道死后的值保留在此供服务侧读;与 worker 通道同签名,
|
|
236
|
+
* 否则默认 fork 通道上 lastProgressFor 缺失 → 断点续试静默失效)。 */
|
|
237
|
+
lastProgressFor(cid) {
|
|
238
|
+
return this.progress.get(cid);
|
|
239
|
+
}
|
|
194
240
|
terminate() {
|
|
195
241
|
if (this.terminated)
|
|
196
242
|
return;
|
|
@@ -222,6 +268,7 @@ class RealChildChannel {
|
|
|
222
268
|
for (const [, entry] of this.pending)
|
|
223
269
|
entry.reject(new Error(error));
|
|
224
270
|
this.pending.clear();
|
|
271
|
+
// progress 不清:#22 断点续试要在崩溃后读到最后的 done(新批起批才清)
|
|
225
272
|
if (!this.terminated && !this.crashed) {
|
|
226
273
|
this.crashed = error;
|
|
227
274
|
this.crashCb?.(error);
|
|
@@ -234,6 +281,21 @@ export class LocalEmbeddingService {
|
|
|
234
281
|
channel;
|
|
235
282
|
entry;
|
|
236
283
|
logger;
|
|
284
|
+
/** 通道重建上下文(构造期固化;respawn 时原样重建,不重新推导预算)。 */
|
|
285
|
+
cfgObj;
|
|
286
|
+
useWorker;
|
|
287
|
+
workerPath;
|
|
288
|
+
childPath;
|
|
289
|
+
injectedChannel;
|
|
290
|
+
respawnOnCrash;
|
|
291
|
+
/** 生效 token 预算(留痕用,见 embedding-source 的 recordTokenBudget 调用点)。 */
|
|
292
|
+
tokenBudgetTokens;
|
|
293
|
+
/** 自愈发起时刻滚动窗(60s ≤2 次;只允许 crashCb 单点读写)。 */
|
|
294
|
+
respawnTimes = [];
|
|
295
|
+
/** 服务侧请求关联号(#22 断点续试:多条目批带 cid,进度按 cid 归属)。 */
|
|
296
|
+
nextCid = 1;
|
|
297
|
+
/** 在飞自愈合流点——非空期间所有崩溃事件归并到同一次重建(防 fork 风暴)。 */
|
|
298
|
+
respawnPromise = null;
|
|
237
299
|
constructor(entry, modelDir, opts) {
|
|
238
300
|
this.entry = entry;
|
|
239
301
|
this.logger = opts.logger;
|
|
@@ -252,31 +314,148 @@ export class LocalEmbeddingService {
|
|
|
252
314
|
maxInputChars,
|
|
253
315
|
maxInputTokens: budget.tokens,
|
|
254
316
|
};
|
|
317
|
+
this.cfgObj = cfgObj;
|
|
318
|
+
this.useWorker = opts.useWorker === true;
|
|
319
|
+
this.workerPath = opts.workerPath ?? defaultWorkerPath();
|
|
320
|
+
this.childPath = opts.childPath ?? defaultChildPath();
|
|
321
|
+
this.injectedChannel = opts.channel;
|
|
322
|
+
this.respawnOnCrash = opts.respawnOnCrash !== false;
|
|
323
|
+
this.tokenBudgetTokens = budget.tokens;
|
|
255
324
|
// #22 取证面:同一份代码在纯 node 能跑、在 Electron-node 崩,而仓内任何地方都没记录
|
|
256
325
|
// execPath 差异与生效预算 —— 下次不用靠系统崩溃报告对时间戳。
|
|
257
326
|
this.logger?.info(`[memory] 本地嵌入子进程拉起:execPath=${process.execPath},release=${process.release?.name ?? 'unknown'},` +
|
|
258
327
|
`model=${entry.id},contextTokens=${entry.contextTokens},tokenBudget=${budget.tokens},` +
|
|
259
328
|
`粗筛 chars=${maxInputChars},通道=${opts.useWorker ? 'worker(旧逃生门,无 token 护栏)' : 'child'}`);
|
|
260
|
-
this.channel =
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
: new RealChildChannel(opts.childPath ?? defaultChildPath(), cfgObj, (pid) => {
|
|
265
|
-
// pid 只能在 fork 后拿到,故补一条(与上方拉起日志同前缀,便于 grep 成对)
|
|
266
|
-
this.logger?.info(`[memory] 本地嵌入子进程 pid=${pid ?? '未知'}(model=${entry.id})`);
|
|
267
|
-
}));
|
|
268
|
-
// 崩溃不自愈(换源/重启恢复):拒绝语义沿 EmbedHelper 降级链走 FTS
|
|
329
|
+
this.channel = this.createChannel();
|
|
330
|
+
// 崩溃有界自愈(#22 机理刀;有意翻转 0.26.25 的「本刀声明不做自愈」——理由与 bound 面
|
|
331
|
+
// 见 RESPAWN_* 两常量与 respawnChannel 注释;注入假通道(测试)不重建:重建工厂会拿到
|
|
332
|
+
// 同一个假实例,自愈无意义且会混淆既有崩溃用例,故假通道维持旧语义)
|
|
269
333
|
this.channel.setOnCrash((error) => {
|
|
270
334
|
if (this.state === 'terminated')
|
|
271
335
|
return;
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
336
|
+
if (!this.respawnOnCrash || this.injectedChannel || !this.respawnAllowed()) {
|
|
337
|
+
this.state = 'failed';
|
|
338
|
+
this.loadError = error;
|
|
339
|
+
this.logger?.warn(`[memory] ${error}(本地嵌入转入 failed 态,换源或重启可恢复)`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.logger?.warn(`[memory] ${error}(#22 自愈第 ${this.respawnTimes.length} 次,60s 窗内 ≤${RESPAWN_MAX_PER_WINDOW})`);
|
|
343
|
+
void this.respawnChannel();
|
|
275
344
|
});
|
|
276
345
|
}
|
|
346
|
+
/** 真通道工厂(构造与自愈共用同一重建路径;假通道注入缝原样返回同一实例,不重建)。 */
|
|
347
|
+
createChannel() {
|
|
348
|
+
if (this.injectedChannel)
|
|
349
|
+
return this.injectedChannel;
|
|
350
|
+
if (this.useWorker)
|
|
351
|
+
return new RealWorkerChannel(this.workerPath, this.cfgObj);
|
|
352
|
+
return new RealChildChannel(this.childPath, this.cfgObj, (pid) => {
|
|
353
|
+
// pid 只能在 fork 后拿到,故补一条(与拉起日志同前缀,便于 grep 成对)
|
|
354
|
+
this.logger?.info(`[memory] 本地嵌入子进程 pid=${pid ?? '未知'}(model=${this.entry.id})`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/** 60s 滚动窗内是否还允许一次自愈(调用即计入,先占位后重建)。 */
|
|
358
|
+
respawnAllowed() {
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
while (this.respawnTimes.length > 0 && now - this.respawnTimes[0] > RESPAWN_WINDOW_MS)
|
|
361
|
+
this.respawnTimes.shift();
|
|
362
|
+
if (this.respawnTimes.length >= RESPAWN_MAX_PER_WINDOW)
|
|
363
|
+
return false;
|
|
364
|
+
this.respawnTimes.push(now);
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* 单点自愈:关旧通道→新通道→重预热→markReady。
|
|
369
|
+
* 不变量:在飞期间 `respawnPromise` 非空且 `state='loading'`(isReady 为假),
|
|
370
|
+
* 现有 vectorsReady 门/EmbedHelper 降级链天然接管半成品窗口,不加新契约态。
|
|
371
|
+
*/
|
|
372
|
+
respawnChannel() {
|
|
373
|
+
if (this.respawnPromise)
|
|
374
|
+
return this.respawnPromise;
|
|
375
|
+
const p = (async () => {
|
|
376
|
+
let next;
|
|
377
|
+
try {
|
|
378
|
+
next = this.createChannel();
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
this.state = 'failed';
|
|
382
|
+
this.loadError = err instanceof Error ? err.message : String(err);
|
|
383
|
+
this.logger?.warn(`[memory] 本地嵌入自愈失败(通道重建): ${this.loadError}`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
// close() 竞态窗口:重建期间被外部释放→新通道不得残留(terminated 后不自愈是铁律)
|
|
387
|
+
if (this.state === 'terminated') {
|
|
388
|
+
next.terminate();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
next.setOnCrash((error) => {
|
|
392
|
+
// 新通道预热完成前暂不挂钩(避免旧通道残景再触发);完成后同路径重装
|
|
393
|
+
if (this.channel !== next || this.state === 'terminated')
|
|
394
|
+
return;
|
|
395
|
+
if (!this.respawnOnCrash || this.injectedChannel || !this.respawnAllowed()) {
|
|
396
|
+
this.state = 'failed';
|
|
397
|
+
this.loadError = error;
|
|
398
|
+
this.logger?.warn(`[memory] ${error}(本地嵌入转入 failed 态,换源或重启可恢复)`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.logger?.warn(`[memory] ${error}(#22 自愈第 ${this.respawnTimes.length} 次,60s 窗内 ≤${RESPAWN_MAX_PER_WINDOW})`);
|
|
402
|
+
void this.respawnChannel();
|
|
403
|
+
});
|
|
404
|
+
this.channel.terminate();
|
|
405
|
+
this.channel = next;
|
|
406
|
+
this.state = 'loading';
|
|
407
|
+
this.loadError = null;
|
|
408
|
+
try {
|
|
409
|
+
await next.request({ type: 'warmup' });
|
|
410
|
+
// 外部 close() 可能在 await 期间把状态推至 terminated(插件卸载/切走);TS 因调用发生在
|
|
411
|
+
// next 上不失效 'loading' 窄化,故显式 cast 回完整并集再比——不能当作不可能分支删掉。
|
|
412
|
+
if (this.state === 'terminated')
|
|
413
|
+
return;
|
|
414
|
+
this.markReady();
|
|
415
|
+
this.logger?.info(`[memory] 本地嵌入自愈完成(第 ${this.respawnTimes.length} 次,60s 窗)`);
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
if (this.state === 'terminated')
|
|
419
|
+
return;
|
|
420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
421
|
+
this.state = 'failed';
|
|
422
|
+
this.loadError = msg;
|
|
423
|
+
this.logger?.warn(`[memory] 本地嵌入自愈失败(重预热): ${msg}`);
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
this.respawnPromise = null;
|
|
427
|
+
}
|
|
428
|
+
})();
|
|
429
|
+
this.respawnPromise = p;
|
|
430
|
+
return p;
|
|
431
|
+
}
|
|
432
|
+
/** 自愈在飞时外部请求的等待缝(带超时;超时/失败都让调用方走既有降级链)。 */
|
|
433
|
+
async awaitRespawn(timeoutMs) {
|
|
434
|
+
const p = this.respawnPromise;
|
|
435
|
+
if (!p)
|
|
436
|
+
return;
|
|
437
|
+
if (!(timeoutMs && timeoutMs > 0)) {
|
|
438
|
+
await p;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
let timer;
|
|
442
|
+
try {
|
|
443
|
+
await Promise.race([p, new Promise((resolve) => {
|
|
444
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
445
|
+
})]);
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
if (timer)
|
|
449
|
+
clearTimeout(timer);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
277
452
|
getDimensions() {
|
|
278
453
|
return this.entry.dims;
|
|
279
454
|
}
|
|
455
|
+
/** 生效的单条 token 预算(=子进程侧 shrinkToBudget 的硬帽;embedding_meta 留痕用)。 */
|
|
456
|
+
getTokenBudget() {
|
|
457
|
+
return this.tokenBudgetTokens;
|
|
458
|
+
}
|
|
280
459
|
getProviderInfo() {
|
|
281
460
|
// dimensions 语义与远程路径一致(sqlite 的 providerInfo 比对含维度)
|
|
282
461
|
return { provider: 'local', model: this.entry.id, dimensions: this.entry.dims };
|
|
@@ -329,7 +508,47 @@ export class LocalEmbeddingService {
|
|
|
329
508
|
this.state = 'loading';
|
|
330
509
|
// 单条请求(召回 query)带优先标记:worker 侧插队,不被 reindex 批次堵队尾
|
|
331
510
|
const startedAt = Date.now();
|
|
332
|
-
const
|
|
511
|
+
const usedChannel = this.channel;
|
|
512
|
+
// 多条目批才带 cid(单条批无断点可续;消息形态对旧通道保持向后兼容)
|
|
513
|
+
const cid = texts.length > 1 ? this.nextCid++ : undefined;
|
|
514
|
+
let reply;
|
|
515
|
+
try {
|
|
516
|
+
reply = await this.requestWithTimeout({ type: 'embed', texts, priority: texts.length === 1, cid }, callOpts?.timeoutMs);
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
// #22 机理刀·断点续试:仅当**所用通道已死**(被杀/崩溃)才触发——超时降级/infer 错误回执/
|
|
520
|
+
// 假通道注入缝都不走此路。旧批条目已进不了库(子进程连同未回执向量一起消失),
|
|
521
|
+
// 但从实时进度拿到的断点位把重做面从「整批」压到「尾部未完段」;只续一次,
|
|
522
|
+
// 再失败走既有 reject——毒输入最多多吃两批,不把补齐链拖进活锁。
|
|
523
|
+
const ack = cid !== undefined && typeof usedChannel.lastProgressFor === 'function' ? usedChannel.lastProgressFor(cid) : undefined;
|
|
524
|
+
const alive = typeof usedChannel.isAlive === 'function' ? usedChannel.isAlive() : true;
|
|
525
|
+
if (!alive &&
|
|
526
|
+
this.respawnOnCrash &&
|
|
527
|
+
!this.injectedChannel &&
|
|
528
|
+
texts.length > 1 &&
|
|
529
|
+
ack !== undefined &&
|
|
530
|
+
ack < texts.length) {
|
|
531
|
+
await this.awaitRespawn(callOpts?.timeoutMs);
|
|
532
|
+
if (this.state === 'ready' || this.state === 'loading') {
|
|
533
|
+
const tail = texts.slice(ack);
|
|
534
|
+
this.logger?.warn(`[memory] 本地嵌入断点续试:model=${this.entry.id} from=${ack}(已 ack 不重放,剩 ${tail.length} 条,仅一次)`);
|
|
535
|
+
const retry = await this.requestWithTimeout({ type: 'embed', texts: tail, priority: false, cid }, callOpts?.timeoutMs);
|
|
536
|
+
if (!retry.ok)
|
|
537
|
+
throw new Error(`断点续试仍失败(from=${ack}): ${retry.error}`);
|
|
538
|
+
if (retry.type !== 'embedded')
|
|
539
|
+
throw new Error(`嵌入通道返回异常消息类型: ${retry.type}`);
|
|
540
|
+
this.markReady();
|
|
541
|
+
for (const v of retry.vectors) {
|
|
542
|
+
if (v.length !== this.entry.dims) {
|
|
543
|
+
throw new Error(`本地嵌入维度不匹配:期望 ${this.entry.dims},得到 ${v.length}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
this.logger?.info(describeEmbedBatch(this.entry.id, tail.length, retry.tokensMax, Date.now() - startedAt));
|
|
547
|
+
return new Array(ack).concat(retry.vectors);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
333
552
|
if (!reply.ok) {
|
|
334
553
|
if (reply.stage === 'load')
|
|
335
554
|
this.applyLoadFailure(reply.error);
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -144,6 +144,14 @@ export declare class MemoryDb {
|
|
|
144
144
|
/** 通用字符串 kv(embedding_meta 表兼作元数据 kv 存储,如 FTS 分词器版本戳)。 */
|
|
145
145
|
private readMetaString;
|
|
146
146
|
private writeMetaString;
|
|
147
|
+
/**
|
|
148
|
+
* #22 口径留痕:记下本库向量是在哪个 token 帽下嵌出来的(生效 tokenBudget)。
|
|
149
|
+
*
|
|
150
|
+
* **只记录不比对、不进 providerInfo drop 判据**(调档不重嵌——帽变化不触发向量失效,
|
|
151
|
+
* 故此键仅作事后取证/排障用,与是否重建索引无关)。写失败静默:留痕是辅助面,
|
|
152
|
+
* 绝不因它拖垮主链路。
|
|
153
|
+
*/
|
|
154
|
+
recordEmbeddingTokenBudget(budget: number): void;
|
|
147
155
|
/**
|
|
148
156
|
* 持久化 embedding meta(语义:物理向量表当前对应的 provider/维度)。
|
|
149
157
|
* 活切换在 swapProvider 成功后即写(表已是新维度);启动/补齐链在
|
package/dist/store/sqlite.js
CHANGED
|
@@ -925,6 +925,23 @@ export class MemoryDb {
|
|
|
925
925
|
.prepare('INSERT INTO embedding_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value')
|
|
926
926
|
.run(key, value);
|
|
927
927
|
}
|
|
928
|
+
/**
|
|
929
|
+
* #22 口径留痕:记下本库向量是在哪个 token 帽下嵌出来的(生效 tokenBudget)。
|
|
930
|
+
*
|
|
931
|
+
* **只记录不比对、不进 providerInfo drop 判据**(调档不重嵌——帽变化不触发向量失效,
|
|
932
|
+
* 故此键仅作事后取证/排障用,与是否重建索引无关)。写失败静默:留痕是辅助面,
|
|
933
|
+
* 绝不因它拖垮主链路。
|
|
934
|
+
*/
|
|
935
|
+
recordEmbeddingTokenBudget(budget) {
|
|
936
|
+
if (!Number.isFinite(budget) || budget <= 0)
|
|
937
|
+
return;
|
|
938
|
+
try {
|
|
939
|
+
this.writeMetaString('embedding_token_budget', String(Math.trunc(budget)));
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
/* 留痕失败不影响功能,静默 */
|
|
943
|
+
}
|
|
944
|
+
}
|
|
928
945
|
/**
|
|
929
946
|
* 持久化 embedding meta(语义:物理向量表当前对应的 provider/维度)。
|
|
930
947
|
* 活切换在 swapProvider 成功后即写(表已是新维度);启动/补齐链在
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-prime-memory",
|
|
3
3
|
"name": "dsh-prime-memory",
|
|
4
|
-
"version": "0.26.
|
|
4
|
+
"version": "0.26.30",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
|
|
7
7
|
"zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|