dsh-prime-memory 0.26.27 → 0.26.29
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 +20 -0
- package/dist/embedding-child.cjs +7 -2
- package/dist/store/local-embedding.d.ts +7 -0
- package/dist/store/local-embedding.js +21 -2
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# 更新日志(Changelog)
|
|
2
2
|
|
|
3
|
+
## [0.26.29] — 2026-11-02
|
|
4
|
+
|
|
5
|
+
> #22 报告人(DESXIE)回验 `0.26.28`:**自动档预算 = 1536、`5×5000` 串行全过**,且用同脚本换 `--budget 2048` 作对照组仍在第 2 条 SIGTRAP——说明“全过”不是环境变了,是 1536 真拦住了。同时他拓出一个真 bug。
|
|
6
|
+
|
|
7
|
+
- **修:`tokensMax=未知` 在真实链路上根本到不了**(报告人实测发现)。子进程侧 `tokensMax` **初值是 0**,而累加用严格大于(`if (cut.seq > tokensMax)`),所以永远量不出 seq 的情形(tokenizer 不可达、预算为 0、护栏被关)得到的 `-1` **永远赢不了 0**;而主线程 `>= 0` 判据会把残留的 0 当有效值印出来——正好印成这行日志本想避免的「一条也没进」。现初值改为 `-1`,“不可证”与“真的零”不再撞同一取值。一行修复,但它使能了整条取证链:`tokensMax` 仅在“护栏被关/tokenizer 不可达”这种最需要告警的时刻才有价值,之前那时它说谎。
|
|
8
|
+
- **为什么没被自己的测试抓到**(比修复本身更值得记):`describeEmbedBatch(…, -1, …)` 的用例只测了**纯函数收到 -1 时印什么**,从来没测**子进程会不会送来 -1**。现已补 3 例:① 哨兵钉死子进程侧初值必须为 -1(结构改动时会提示同步);② 复现归并语义本身(初值 0 吃掉了不可证、初值 -1 不会,且任一有效值仍能抬高);③ 端到端走真 `shrinkToBudget`(预算 0 时)→ `-1` → 印「未知」。已反向验证:把初值改回 0,哨兵即转红。
|
|
9
|
+
- **报告人的其他三项标注(无需改动,但记下来防重复讨论)**:`code/signal` 归因与「停摆 warn + 带 record id 的失败日志」与 `0.26.25` 逐字相同,属**既有实现而非本批新增**(他说得对,我之前的行文容易让人误读成每版新修);`tokensMax` 独立复核与自报值一致(1535/2047 两档均双路吻合),护栏真截了。
|
|
10
|
+
- **采纳他的口径澄清**:`graph` 投影那串「连发」是**连续多次单条 `priority` 请求**(所以日志里 `n` 常见为 1),真正成批多条的只有 reindex(`CHUNK=16`);这不影响护栏结论,但读 `tokensMax` 时应知道 `n` 的含义。机理本身仍未动(1536 是实测经验值,不是证明)。
|
|
11
|
+
- 全量 **1938 例 / 191 文件**全绿(+3);typecheck 0、lint 0 error、build/build:smoke/smoke/verify-catalog 均 0。公开配置面无变更。
|
|
12
|
+
|
|
13
|
+
## [0.26.28] — 2026-11-02
|
|
14
|
+
|
|
15
|
+
> 本版只修一个错:**我在 `0.26.26`/`0.26.27` 的 CHANGELOG 与 issue 留言里说「子进程上报本批实测进模型的最大 token 数」,但那个值从来没进过日志**。它只被本地验证脚本的探针读到,插件自己的 `memory.log` 里一行都没有——而 #22 这类问题事后能核的只有日志。计划本身是要求这一行落盘的(每批 info 记 `n / 最大tokens / inferMs`,fork 时记 `pid`),是我执行时漏了两项。
|
|
16
|
+
|
|
17
|
+
- **每批推理现在真会落一行 info**(`describeEmbedBatch()`):`[memory] 本地嵌入一批完成:model=<id> n=<条数>,进模型 tokensMax=<实测值>,耗时=<ms>`。三个细节:① `tokensMax` 是子进程按库口径**实测**回来的值,不是推算;② 不可证(子进程给 -1)时印「未知」而不是 0,避开「一条也没进」的误读;③ 用 `info` 而非 `debug`(`src/util/filelog.ts` 只镜像 info+,debug 根本进不了 `memory.log`)。这样报告人按我给的清单回验时,「日志里每批 `tokensMax ≤ 预算`」这一条才真的可验。
|
|
18
|
+
- **fork 后补记子进程 `pid`**(`RealChildChannel` 新增 `onForked` 回调):#22 的归因本来就是靠「系统崩溃报告 ↔ 插件日志」对时间戳,有 pid 才能把一份 `.ips` 对上哪个子进程,`taskkill /T` 的进程树也因此可核。原先只有 `execPath/release/model/tokenBudget`,没 pid。
|
|
19
|
+
- **协议类型同步**:`EmbedWorkerReply` 的 `embedded` 分支加上可选 `tokensMax`,让这个值在主线程类型可见(不再是只存在于未声明字段的 JSON 里)。
|
|
20
|
+
- **验证**:`scripts/verify-token-guard-realmodel.mjs` 的端到端段现在**直接断言真落盘的日志行**(而不只是内存里取个值):拉起行含 `execPath`+`tokenBudget`、另有一行 `pid=\d+`、每批一行 `tokensMax` 且均 ≤ 预算。实测:`pid=37384`、两批 `tokensMax=1535,1535`(预算 1536)、混批与同文本长批均正常返回 1024 维向量。新增 CI 用例 2 例(每批日志内容断言 + 「不可证记未知」边界),本刀累计 31 例。
|
|
21
|
+
- 全量 **1935 例 / 191 文件**全绿;typecheck 0、lint 0 error、build/build:smoke/smoke/verify-catalog 均 0。无配置面变更,无破坏性变更。
|
|
22
|
+
|
|
3
23
|
## [0.26.27] — 2026-11-02
|
|
4
24
|
|
|
5
25
|
> #22 报告人在 macOS/M5 上回验了 `0.26.25`:**三个原致死档不再 SIGTRAP、护栏确实接上**(这验证了主线结论),但翻出一个**比原缺陷更贴近真实负载**的问题,本版纳入他的二分实测。
|
package/dist/embedding-child.cjs
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* - 出向:{id,ok:true,type:'pong'|'ready'|'embedded',vectors?,tokensMax?}(vectors 为
|
|
16
16
|
* number[][],fork IPC 默认 JSON 序列化不支持 TypedArray,主线程侧重建
|
|
17
17
|
* Float32Array;512d×32 条 ≈64KB,序列化 <5ms,瓶颈在推理不在传输)。
|
|
18
|
-
* tokensMax =
|
|
18
|
+
* tokensMax = 本批**实测**进模型的最大 token 数(#22 护栏自证用,主线程不消费也不报错);
|
|
19
|
+
* 初值为 -1 而非 0 —— -1 表示「不可证」,与「真的一个 token 也没进」必须区分开。
|
|
19
20
|
* | {id,ok:false,stage:'load'|'infer',error} | {type:'fatal',error}
|
|
20
21
|
*
|
|
21
22
|
* 信任边界(沿 AGENTS.md 审查记录):runtime/ 目录在数据目录信任边界内,
|
|
@@ -173,7 +174,11 @@ function handleEmbed(msg) {
|
|
|
173
174
|
postReply({ id: msg.id, ok: true, type: 'embedded', vectors: [] });
|
|
174
175
|
return;
|
|
175
176
|
}
|
|
176
|
-
|
|
177
|
+
// tokensMax 初值必须是 -1(不可证)而不是 0:下面的累加用严格大于,若初值为 0,
|
|
178
|
+
// 则永远量不出 seq 的情形(tokenizer 不可达/预算为 0)会把 -1 压成 0,
|
|
179
|
+
// 而主线程的 `>= 0` 判据会把 0 当有效值印出来——正好印成「一条也没进」,
|
|
180
|
+
// 与「不可证」撞车,使这行日志在最需要它的时刻说谎(#22 报告人回验实测)。
|
|
181
|
+
const request = { id: msg.id, vectors: new Array(texts.length), remaining: texts.length, failed: false, tokensMax: -1 };
|
|
177
182
|
// 单条请求(召回 query)插队到队首;批次追加到队尾(批内条目间仍会被后来的插队)
|
|
178
183
|
const priority = msg.priority === true && texts.length === 1;
|
|
179
184
|
for (let i = 0; i < texts.length; i++) {
|
|
@@ -40,6 +40,7 @@ export type EmbedWorkerReply = {
|
|
|
40
40
|
ok: true;
|
|
41
41
|
type: 'embedded';
|
|
42
42
|
vectors: Float32Array[];
|
|
43
|
+
tokensMax?: number;
|
|
43
44
|
} | {
|
|
44
45
|
id: number;
|
|
45
46
|
ok: false;
|
|
@@ -60,6 +61,12 @@ export interface EmbedWorkerChannel {
|
|
|
60
61
|
* signal 同时进 crashed 串、向上传为 loadError,故一处修三处可见。
|
|
61
62
|
*/
|
|
62
63
|
export declare function describeExit(what: string, code: number | null, signal: string | null): string;
|
|
64
|
+
/**
|
|
65
|
+
* 每批推理的可见性行(info 才能进 memory.log)。`tokensMax` 是子进程**实测**上报的
|
|
66
|
+
* 「本轮真正进模型的最大 token 数」(-1 = 不可证):把它落盘才能事后回答
|
|
67
|
+
* 「护栏当时到底生效了没有」,否则它只存在于本地脚本的探针里。
|
|
68
|
+
*/
|
|
69
|
+
export declare function describeEmbedBatch(modelId: string, n: number, tokensMax: number | undefined, ms: number): string;
|
|
63
70
|
export interface LocalEmbeddingOptions {
|
|
64
71
|
/** 数据目录 runtime/(推理侧据此 createRequire 加载 transformers)。 */
|
|
65
72
|
runtimeDir: string;
|
|
@@ -108,6 +108,15 @@ class RealWorkerChannel {
|
|
|
108
108
|
export function describeExit(what, code, signal) {
|
|
109
109
|
return `${what}退出(code=${code ?? 'null'},signal=${signal ?? 'null'})`;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* 每批推理的可见性行(info 才能进 memory.log)。`tokensMax` 是子进程**实测**上报的
|
|
113
|
+
* 「本轮真正进模型的最大 token 数」(-1 = 不可证):把它落盘才能事后回答
|
|
114
|
+
* 「护栏当时到底生效了没有」,否则它只存在于本地脚本的探针里。
|
|
115
|
+
*/
|
|
116
|
+
export function describeEmbedBatch(modelId, n, tokensMax, ms) {
|
|
117
|
+
const tok = typeof tokensMax === 'number' && tokensMax >= 0 ? String(tokensMax) : '未知';
|
|
118
|
+
return `[memory] 本地嵌入一批完成:model=${modelId} n=${n},进模型 tokensMax=${tok},耗时=${ms}ms`;
|
|
119
|
+
}
|
|
111
120
|
/**
|
|
112
121
|
* 真实子进程通道(fix #11 默认):fork 独立进程隔离 sharp/libvips DLL 冲突。
|
|
113
122
|
* 与 RealWorkerChannel 同协议语义,差异仅在传输层:
|
|
@@ -124,13 +133,16 @@ class RealChildChannel {
|
|
|
124
133
|
terminated = false;
|
|
125
134
|
crashed;
|
|
126
135
|
crashCb;
|
|
127
|
-
constructor(childPath, cfg) {
|
|
136
|
+
constructor(childPath, cfg, onForked) {
|
|
128
137
|
this.child = fork(childPath, [], {
|
|
129
138
|
// execPath 显式沿用当前二进制:宿主 Electron 自带 Node runtime,打包后 PATH 里没有 node
|
|
130
139
|
execPath: process.execPath,
|
|
131
140
|
env: { ...process.env, DSH_EMBED_CFG: JSON.stringify(cfg) },
|
|
132
141
|
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
133
142
|
});
|
|
143
|
+
// pid 必须落日志:#22 报告人就是靠「崩溃报告 ↔ 日志」的时间对撞才归因上的,而有 pid
|
|
144
|
+
// 才能把一份系统崩溃报告对上哪个子进程(taskkill /T 的进程树也因此可核)。
|
|
145
|
+
onForked?.(this.child.pid);
|
|
134
146
|
this.child.on('message', (msg) => {
|
|
135
147
|
if (msg && msg.type === 'fatal') {
|
|
136
148
|
this.failAll(`本地嵌入子进程致命错误: ${msg.error ?? '未知'}`);
|
|
@@ -249,7 +261,10 @@ export class LocalEmbeddingService {
|
|
|
249
261
|
opts.channel ??
|
|
250
262
|
(opts.useWorker
|
|
251
263
|
? new RealWorkerChannel(opts.workerPath ?? defaultWorkerPath(), cfgObj)
|
|
252
|
-
: new RealChildChannel(opts.childPath ?? defaultChildPath(), cfgObj)
|
|
264
|
+
: new RealChildChannel(opts.childPath ?? defaultChildPath(), cfgObj, (pid) => {
|
|
265
|
+
// pid 只能在 fork 后拿到,故补一条(与上方拉起日志同前缀,便于 grep 成对)
|
|
266
|
+
this.logger?.info(`[memory] 本地嵌入子进程 pid=${pid ?? '未知'}(model=${entry.id})`);
|
|
267
|
+
}));
|
|
253
268
|
// 崩溃不自愈(换源/重启恢复):拒绝语义沿 EmbedHelper 降级链走 FTS
|
|
254
269
|
this.channel.setOnCrash((error) => {
|
|
255
270
|
if (this.state === 'terminated')
|
|
@@ -313,6 +328,7 @@ export class LocalEmbeddingService {
|
|
|
313
328
|
if (this.state !== 'ready')
|
|
314
329
|
this.state = 'loading';
|
|
315
330
|
// 单条请求(召回 query)带优先标记:worker 侧插队,不被 reindex 批次堵队尾
|
|
331
|
+
const startedAt = Date.now();
|
|
316
332
|
const reply = await this.requestWithTimeout({ type: 'embed', texts, priority: texts.length === 1 }, callOpts?.timeoutMs);
|
|
317
333
|
if (!reply.ok) {
|
|
318
334
|
if (reply.stage === 'load')
|
|
@@ -329,6 +345,9 @@ export class LocalEmbeddingService {
|
|
|
329
345
|
throw new Error(`本地嵌入维度不匹配:期望 ${this.entry.dims},得到 ${v.length}`);
|
|
330
346
|
}
|
|
331
347
|
}
|
|
348
|
+
// 每批一行 info(#22 取证面):子进程**实测**进模型的 tokensMax 必须落盘,
|
|
349
|
+
// 否则事后无法回答「护栏当时生效了没」;耗时兼作 EMA 熔断之外的裸值参考。
|
|
350
|
+
this.logger?.info(describeEmbedBatch(this.entry.id, texts.length, reply.tokensMax, Date.now() - startedAt));
|
|
332
351
|
return reply.vectors;
|
|
333
352
|
}
|
|
334
353
|
/** 释放推理子进程与模型(嵌入源切走/关闭时调用;幂等)。terminated 后不可
|
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.29",
|
|
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 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|