dsh-prime-memory 0.26.26 → 0.26.28

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # 更新日志(Changelog)
2
2
 
3
+ ## [0.26.28] — 2026-11-02
4
+
5
+ > 本版只修一个错:**我在 `0.26.26`/`0.26.27` 的 CHANGELOG 与 issue 留言里说「子进程上报本批实测进模型的最大 token 数」,但那个值从来没进过日志**。它只被本地验证脚本的探针读到,插件自己的 `memory.log` 里一行都没有——而 #22 这类问题事后能核的只有日志。计划本身是要求这一行落盘的(每批 info 记 `n / 最大tokens / inferMs`,fork 时记 `pid`),是我执行时漏了两项。
6
+
7
+ - **每批推理现在真会落一行 info**(`describeEmbedBatch()`):`[memory] 本地嵌入一批完成:model=<id> n=<条数>,进模型 tokensMax=<实测值>,耗时=<ms>`。三个细节:① `tokensMax` 是子进程按库口径**实测**回来的值,不是推算;② 不可证(子进程给 -1)时印「未知」而不是 0,避开「一条也没进」的误读;③ 用 `info` 而非 `debug`(`src/util/filelog.ts` 只镜像 info+,debug 根本进不了 `memory.log`)。这样报告人按我给的清单回验时,「日志里每批 `tokensMax ≤ 预算`」这一条才真的可验。
8
+ - **fork 后补记子进程 `pid`**(`RealChildChannel` 新增 `onForked` 回调):#22 的归因本来就是靠「系统崩溃报告 ↔ 插件日志」对时间戳,有 pid 才能把一份 `.ips` 对上哪个子进程,`taskkill /T` 的进程树也因此可核。原先只有 `execPath/release/model/tokenBudget`,没 pid。
9
+ - **协议类型同步**:`EmbedWorkerReply` 的 `embedded` 分支加上可选 `tokensMax`,让这个值在主线程类型可见(不再是只存在于未声明字段的 JSON 里)。
10
+ - **验证**:`scripts/verify-token-guard-realmodel.mjs` 的端到端段现在**直接断言真落盘的日志行**(而不只是内存里取个值):拉起行含 `execPath`+`tokenBudget`、另有一行 `pid=\d+`、每批一行 `tokensMax` 且均 ≤ 预算。实测:`pid=37384`、两批 `tokensMax=1535,1535`(预算 1536)、混批与同文本长批均正常返回 1024 维向量。新增 CI 用例 2 例(每批日志内容断言 + 「不可证记未知」边界),本刀累计 31 例。
11
+ - 全量 **1935 例 / 191 文件**全绿;typecheck 0、lint 0 error、build/build:smoke/smoke/verify-catalog 均 0。无配置面变更,无破坏性变更。
12
+
13
+ ## [0.26.27] — 2026-11-02
14
+
15
+ > #22 报告人在 macOS/M5 上回验了 `0.26.25`:**三个原致死档不再 SIGTRAP、护栏确实接上**(这验证了主线结论),但翻出一个**比原缺陷更贴近真实负载**的问题,本版纳入他的二分实测。
16
+
17
+ - **`HARD_CAP_TOKENS` 2048 → 1536:预算原本只对了「单发」,而本仓真实负载是「连发」**。报告人二分实测(Electron-node,`5×5000` 中文字符串行):**512/1024 全过、1900/1960 全过、1990/2000/2010/2016/2020/2040/2048 均「第 1 条成功、第 2 条 SIGTRAP」**;而单发致死线是 ≈2700。两者不同源的机理止得住我的原分析:**BFCArena 高水位不回落**(我先前已测到 6 轮大推理后 RSS plateau 不降),第二条进来时第一份 T² 张量尚未回收完,**新块与旧块共存瞬间的峰值**才是真正的天花板。
18
+ - **为什么这条要紧(已实地核实,不是直按报告人的话)**:`src/pipeline/graph.ts:134-137` 正是 `for (const n of touched) { … await embed(nodeEmbedText(n)) … }` —— 逐条串行且**单条请求**,一个投影批里常有 5~8 个节点,其中两条长就命中。所以原issue 的「每次投影必崩」在 2048 下只降到「命中≥2 条长文本时崩」,而**本刀已声明不做自愈**,命中仍是崩一次废到重启。1536 在连发线(≈1960)下留 ≈21% 余量(报告人建议值,他也给了 1024 选项;取 1536 是为了不一刀牺牲 bge-m3 长文本的召回语义)。
19
+ - **T1 脚本新增「连发」维度**(`scripts/verify-token-guard-realmodel.mjs --serial N`):同一段 5000 字符在**同一 pipeline 实例**里串行喂 5 次,断言**每一轮实测 seq 均 ≤ 预算且各轮一致**。本机(Windows/纯 node)不会 trap,所以这只能证「预算对连发一致成立」,不能证「连发不崩」(后者只能 macOS)。实测:`seqs=1535,1535,1535,1535,1535`(预算 1536);T3 峰值从 1469MB 降至 **1235MB**(无护栏仍 4168MB)。
20
+ - **收干自己造下的魔数漂移面**(这是 `0.26.25/26` 留下的):测试与脚本里原本**把 2048 当字面量贴了 8 处**,改常量时全变成假红/假绿。现一律从 `HARD_CAP_TOKENS` 推导,真模型脚本也从 `dist/model-catalog.js` 读默认值;哨兵断言同时禁止 `1536|2048|2700` 出现在子进程/主进程层。**诚实记录:改完从常量推导后,我立刻发现自己两处算错而实现的断言**(用魔数时我把错误算成了和实现一致的值,所以“看起来绿”),现在改成断**契约性质**(收到预算内 + 真的截了 + 首轮不多试)而不是断精确切片长度。
21
+ - **文档同步**:9 语 README 的 `embedding.maxInputTokens` 行由「硬上限 2048」改为 1536;`src/config.ts` 对该键的描述不含具体数字(避免下一处漂移)。
22
+ - 另接受报告人一个诊断修正:他指出 `tokensMax` 在 `0.26.25` 恒为 0、且 `Array.isArray(shape)` 分支「永远进不去」——与我在 `0.26.26` 独立查到的同一缺陷一致(他比我先跑出这个结论,他是从外部拿真 tokenizer 量的)。
23
+ - **测试**:`tests/embedding-token-guard.test.ts` 保持 **29 例**(断言形式从魔数改为常量推导);全量 **1933 例 / 191 文件**全绿,typecheck 0 error、lint 0 error、build/build:smoke/smoke/verify-catalog 均 0。公开配置面无破坏性变更(仅默认推导出的预算从 2048 收紧到 1536,显式设过 `maxInputTokens` 的用户不受影响)。
24
+
3
25
  ## [0.26.26] — 2026-11-02
4
26
 
5
27
  > `0.26.25` 把 #22 的 token 护栏落地了,但把**最关键的那条验证当成了「已接上报」而非「已实测」**。本版用真 bge-m3 把它跑完,跑出了三个真实缺陷并逐个修掉。
package/README.de.md CHANGED
@@ -323,7 +323,7 @@ Drei Embedding-Quellen: **aus** (Standard, reine BM25-Schlüsselwortsuche), **re
323
323
  | `embedding.model` | leer | Embedding-Modellname |
324
324
  | `embedding.dimensions` | `0` | Vektordimension (bei Aktivierung Pflicht, muss zur Modellausgabe passen) |
325
325
  | `embedding.maxInputChars` | `5000` | Maximale Zeichen je Text (bei Überlänge abgeschnitten) |
326
- | `embedding.maxInputTokens` | `0` | **Token**-Budget pro Text für lokale Einbettung: `0` (Standard) = min(Modellfenster, harte Grenze 2048); >0 = explizit, bei Überschreitung geklemmt inkl. warn. Andere Rolle als die Zeile oben – das Zeichenlimit betrifft nur Remote-Modus & IPC-Größe, die lokale Absturz-/Speichergrenze muss in Tokens stehen (#22) |
326
+ | `embedding.maxInputTokens` | `0` | **Token**-Budget pro Text für lokale Einbettung: `0` (Standard) = min(Modellfenster, harte Grenze 1536); >0 = explizit, bei Überschreitung geklemmt inkl. warn. Andere Rolle als die Zeile oben – das Zeichenlimit betrifft nur Remote-Modus & IPC-Größe, die lokale Absturz-/Speichergrenze muss in Tokens stehen (#22) |
327
327
  | `embedding.timeoutMs` | `10000` | Timeout eines einzelnen Embedding-Aufrufs (ms) |
328
328
  | `embedding.allowLocalModels` | `true` | Lokale Embedding-Stufe erlauben (Deployment-Obergrenze: ausgeschaltet kann die Einstellungsseite keine Modelle laden und nicht auf Lokal schalten) |
329
329
  | `embedding.mirror` | `https://hf-mirror.com` | Wurzeladresse des Download-Spiegels für lokale Modelle (zurücksetzbar auf das offizielle `https://huggingface.co`) |
package/README.en.md CHANGED
@@ -330,7 +330,7 @@ the bundle layer appends and causes `duplicate loader entry id` startup failure)
330
330
  | `embedding.model` | empty | embedding model name |
331
331
  | `embedding.dimensions` | `0` | Vector dimensions (required when enabled; must match model output) |
332
332
  | `embedding.maxInputChars` | `5000` | Max characters per text (overlong inputs truncated) |
333
- | `embedding.maxInputTokens` | `0` | Per-text **token** budget for local embedding: `0` (default) = min(model window, hard cap 2048); >0 = explicit, clamped with a warn above the cap. Not the same guard as the row above — the character cap only bounds the remote tier and IPC size, while local crash/memory limits must be in tokens (#22) |
333
+ | `embedding.maxInputTokens` | `0` | Per-text **token** budget for local embedding: `0` (default) = min(model window, hard cap 1536); >0 = explicit, clamped with a warn above the cap. Not the same guard as the row above — the character cap only bounds the remote tier and IPC size, while local crash/memory limits must be in tokens (#22) |
334
334
  | `embedding.timeoutMs` | `10000` | Per-call embedding timeout (ms) |
335
335
  | `embedding.allowLocalModels` | `true` | Allow the local embedding tier (deployment ceiling; when off, no model downloads and no local tier in settings) |
336
336
  | `embedding.mirror` | `https://hf-mirror.com` | Download mirror root for local models (can be changed back to `https://huggingface.co`) |
package/README.es.md CHANGED
@@ -324,7 +324,7 @@ Los ajustes que pisan los valores por defecto se escriben en el `cordis.patch.ym
324
324
  | `embedding.model` | vacío | Nombre del modelo de embeddings |
325
325
  | `embedding.dimensions` | `0` | Dimensión de los vectores (obligatoria si está activada, debe coincidir con la salida del modelo) |
326
326
  | `embedding.maxInputChars` | `5000` | Número máximo de caracteres por texto (recorte si se supera) |
327
- | `embedding.maxInputTokens` | `0` | Presupuesto de **tokens** por texto para la incrustación local: `0` (pred.) = min(ventana del modelo, límite 2048); >0 = explícito, recortado con warn si excede. No es el mismo límite que la fila de arriba: el de caracteres solo cubre el modo remoto y el tamaño de IPC, y la protección local contra fallos/memoria debe ir en tokens (#22) |
327
+ | `embedding.maxInputTokens` | `0` | Presupuesto de **tokens** por texto para la incrustación local: `0` (pred.) = min(ventana del modelo, límite 1536); >0 = explícito, recortado con warn si excede. No es el mismo límite que la fila de arriba: el de caracteres solo cubre el modo remoto y el tamaño de IPC, y la protección local contra fallos/memoria debe ir en tokens (#22) |
328
328
  | `embedding.timeoutMs` | `10000` | Tiempo máximo de una llamada de embeddings (ms) |
329
329
  | `embedding.allowLocalModels` | `true` | Permite el modo de embeddings local (techo de despliegue: desactivado, la página de ajustes no puede descargar modelos ni pasar a local) |
330
330
  | `embedding.mirror` | `https://hf-mirror.com` | Raíz del espejo de descarga de modelos locales (se puede devolver al oficial `https://huggingface.co`) |
package/README.fr.md CHANGED
@@ -322,7 +322,7 @@ Les réglages par-dessus les valeurs par défaut s'écrivent dans le `cordis.pat
322
322
  | `embedding.model` | vide | Nom du modèle d'embedding |
323
323
  | `embedding.dimensions` | `0` | Dimension des vecteurs (obligatoire si activé, doit correspondre à la sortie du modèle) |
324
324
  | `embedding.maxInputChars` | `5000` | Nombre maximal de caractères par texte (troncature au-delà) |
325
- | `embedding.maxInputTokens` | `0` | Budget de **jetons** par texte pour l'intégration locale : `0` (défaut) = min(fenêtre du modèle, plafond 2048) ; >0 = explicite, réduit avec un warn s'il dépasse. Ce n'est pas la même limite que la ligne au-dessus : celle en caractères ne borne que le mode distant et la taille IPC, la protection locale doit être en jetons (#22) |
325
+ | `embedding.maxInputTokens` | `0` | Budget de **jetons** par texte pour l'intégration locale : `0` (défaut) = min(fenêtre du modèle, plafond 1536) ; >0 = explicite, réduit avec un warn s'il dépasse. Ce n'est pas la même limite que la ligne au-dessus : celle en caractères ne borne que le mode distant et la taille IPC, la protection locale doit être en jetons (#22) |
326
326
  | `embedding.timeoutMs` | `10000` | Délai maximal d'un appel d'embedding (ms) |
327
327
  | `embedding.allowLocalModels` | `true` | Autorise le mode embedding local (plafond de déploiement : désactivé, la page de paramètres ne peut plus télécharger de modèles ni passer en local) |
328
328
  | `embedding.mirror` | `https://hf-mirror.com` | Racine du miroir de téléchargement des modèles locaux (ramenérable à l'officiel `https://huggingface.co`) |
package/README.it.md CHANGED
@@ -324,7 +324,7 @@ Le impostazioni che scavalcano i predefiniti si scrivono nel `cordis.patch.yml`
324
324
  | `embedding.model` | vuoto | Nome del modello d'embedding |
325
325
  | `embedding.dimensions` | `0` | Dimensione dei vettori (obbligatoria se attiva, deve corrispondere all'output del modello) |
326
326
  | `embedding.maxInputChars` | `5000` | Numero massimo di caratteri per testo (troncamento oltre) |
327
- | `embedding.maxInputTokens` | `0` | Budget di **token** per testo per l'embedding locale: `0` (default) = min(finestra del modello, limite 2048); >0 = esplicito, ridotto con un warn se oltre il limite. Non è lo stesso limite della riga sopra: quello in caratteri copre solo modalità remota e dimensione IPC, la protezione locale contro cadute/memoria deve essere in token (#22) |
327
+ | `embedding.maxInputTokens` | `0` | Budget di **token** per testo per l'embedding locale: `0` (default) = min(finestra del modello, limite 1536); >0 = esplicito, ridotto con un warn se oltre il limite. Non è lo stesso limite della riga sopra: quello in caratteri copre solo modalità remota e dimensione IPC, la protezione locale contro cadute/memoria deve essere in token (#22) |
328
328
  | `embedding.timeoutMs` | `10000` | Timeout di una singola chiamata d'embedding (ms) |
329
329
  | `embedding.allowLocalModels` | `true` | Consente la modalità d'embedding locale (tetto di distribuzione: disattivata, la pagina impostazioni non può scaricare modelli né passare in locale) |
330
330
  | `embedding.mirror` | `https://hf-mirror.com` | Radice del mirror di scaricamento dei modelli locali (riportabile all'ufficiale `https://huggingface.co`) |
package/README.ja.md CHANGED
@@ -274,7 +274,7 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
274
274
  | `embedding.model` | 空 | 埋め込みモデル名 |
275
275
  | `embedding.dimensions` | `0` | ベクトル次元(有効時必須) |
276
276
  | `embedding.maxInputChars` | `5000` | 単テキスト最大文字数(超長は切り捨て) |
277
- | `embedding.maxInputTokens` | `0` | ローカル埋め込みの 1 件あたり **トークン** 予算:`0`(既定)= モデル窓と上限 2048 の小さい方、>0 = 明示値(超過は clamp + warn)。上の行とは役割が異なる——文字上限はリモートモードと IPC サイズのみを制限し、ローカルのクラッシュ/メモリ防護はトークン単位が必須(#22) |
277
+ | `embedding.maxInputTokens` | `0` | ローカル埋め込みの 1 件あたり **トークン** 予算:`0`(既定)= モデル窓と上限 1536 の小さい方、>0 = 明示値(超過は clamp + warn)。上の行とは役割が異なる——文字上限はリモートモードと IPC サイズのみを制限し、ローカルのクラッシュ/メモリ防護はトークン単位が必須(#22) |
278
278
  | `embedding.timeoutMs` | `10000` | 単回埋め込み呼び出しタイムアウト(ms) |
279
279
  | `embedding.allowLocalModels` | `true` | ローカル埋め込み档を許可(デプロイ上限) |
280
280
  | `embedding.mirror` | `https://hf-mirror.com` | ローカルモデルダウンロードミラー根 |
package/README.ko.md CHANGED
@@ -274,7 +274,7 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
274
274
  | `embedding.model` | 빈값 | 임베딩 모델명 |
275
275
  | `embedding.dimensions` | `0` | 벡터 차원(활성 시 필수) |
276
276
  | `embedding.maxInputChars` | `5000` | 단텍스트 최대 문자 수(초장 잘림) |
277
- | `embedding.maxInputTokens` | `0` | 로컬 임베딩 1건당 **토큰** 예산: `0`(기본) = 모델 창과 상한 2048 중 작은 값, >0 = 명시값(초과 시 clamp + warn). 위 행과 역할이 다름 — 문자 상한은 원격 모드와 IPC 크기만 제한하고, 로컬 충돌/메모리 방지는 토큰 단위가 필수(#22) |
277
+ | `embedding.maxInputTokens` | `0` | 로컬 임베딩 1건당 **토큰** 예산: `0`(기본) = 모델 창과 상한 1536 중 작은 값, >0 = 명시값(초과 시 clamp + warn). 위 행과 역할이 다름 — 문자 상한은 원격 모드와 IPC 크기만 제한하고, 로컬 충돌/메모리 방지는 토큰 단위가 필수(#22) |
278
278
  | `embedding.timeoutMs` | `10000` | 단회 임베딩 호출 타임아웃(ms) |
279
279
  | `embedding.allowLocalModels` | `true` | 로컬 임베딩档 허용(배포 상한) |
280
280
  | `embedding.mirror` | `https://hf-mirror.com` | 로컬 모델 다운로드 미러 루트 |
package/README.md CHANGED
@@ -342,7 +342,7 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
342
342
  | `embedding.model` | 空 | embedding 模型名 |
343
343
  | `embedding.dimensions` | `0` | 向量维度(启用时必填,须与模型输出一致) |
344
344
  | `embedding.maxInputChars` | `5000` | 单条文本最大字符数(超长截断) |
345
- | `embedding.maxInputTokens` | `0` | 本地嵌入的单条 **token** 预算:`0`(默认)= 按模型窗口与硬上限 2048 自动取小;>0 = 显式预算,超上限会被 clamp 并记 warn。与上一行不是同类护栏——字符帽只管远程档与 IPC 体积,本地推理的崩溃/内存护栏必须按 token(#22) |
345
+ | `embedding.maxInputTokens` | `0` | 本地嵌入的单条 **token** 预算:`0`(默认)= 按模型窗口与硬上限 1536 自动取小;>0 = 显式预算,超上限会被 clamp 并记 warn。与上一行不是同类护栏——字符帽只管远程档与 IPC 体积,本地推理的崩溃/内存护栏必须按 token(#22) |
346
346
  | `embedding.timeoutMs` | `10000` | 单次 embedding 调用超时(ms) |
347
347
  | `embedding.allowLocalModels` | `true` | 允许本地嵌入档(部署上限:关闭后设置页不能下载模型、不能切本地档) |
348
348
  | `embedding.mirror` | `https://hf-mirror.com` | 本地模型下载镜像根地址(可改回官方 `https://huggingface.co`) |
package/README.ru.md CHANGED
@@ -324,7 +324,7 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
324
324
  | `embedding.model` | пусто | Имя модели эмбеддингов |
325
325
  | `embedding.dimensions` | `0` | Размерность векторов (при включении обязательна, должна совпадать с выходом модели) |
326
326
  | `embedding.maxInputChars` | `5000` | Максимум символов на один текст (сверх — усечение) |
327
- | `embedding.maxInputTokens` | `0` | Бюджет **токенов** на один текст для локальных эмбеддингов: `0` (умолч.) = min(окно модели, жёсткий лимит 2048); >0 = явное значение, при превыщении усекается с warn. Это не тот же лимит, что строкой выше: символьный ограничивает только удалённый режим и размер IPC, а локальная защита от падений/памяти должна считаться в токенах (#22) |
327
+ | `embedding.maxInputTokens` | `0` | Бюджет **токенов** на один текст для локальных эмбеддингов: `0` (умолч.) = min(окно модели, жёсткий лимит 1536); >0 = явное значение, при превыщении усекается с warn. Это не тот же лимит, что строкой выше: символьный ограничивает только удалённый режим и размер IPC, а локальная защита от падений/памяти должна считаться в токенах (#22) |
328
328
  | `embedding.timeoutMs` | `10000` | Таймаут одного вызова эмбеддинга (мс) |
329
329
  | `embedding.allowLocalModels` | `true` | Разрешить локальный режим эмбеддингов (потолок развёртывания: выключено — страница настроек не может скачивать модели и переключаться в локальный) |
330
330
  | `embedding.mirror` | `https://hf-mirror.com` | Корень зеркала загрузки локальных моделей (можно вернуть на официальный `https://huggingface.co`) |
@@ -150,8 +150,8 @@ async function pump() {
150
150
  const data = result[0] && result[0].data;
151
151
  job.request.vectors[job.index] = data instanceof Float32Array ? data : new Float32Array(data);
152
152
  // tokensMax 用 cut.seq(按库口径实测将要进模型的 seq 长度,含 special token)。
153
- // 曾试过从**输出**张量取 shape——真 bge-m3 实测证明两事:① Tensor 上根本没有 `.shape`
154
- // (叫 `.dims`),② 池化后输出是 `[1024]` 一维,里面不存在 seq ——旧写法恒为 -1,等于没上报。
153
+ // 曾试过从**输出**张量取形状——真 bge-m3 实测证明两事:① Tensor 上根本没有 `.shape`
154
+ // (属性叫 `.dims`),② 池化后输出是一维向量,里面不存在 seq 维——旧写法永远拿不到值。
155
155
  // 不可证时为 -1(不是 0),不拿假数据当护栏证据。
156
156
  if (cut.seq > job.request.tokensMax) job.request.tokensMax = cut.seq;
157
157
  job.request.remaining -= 1;
@@ -14,10 +14,10 @@
14
14
  * 的 run() 是同步阻塞调用——单条内部不可中断,预检只能放推理前。又因为「子进程隔离」本身
15
15
  * 是 #11(libvips-42.dll 同进程撞名)的修复,不得为省一次 IPC 把 tokenize 挪回主线程。
16
16
  *
17
- * 为什么不靠「budget - 2 留位」这种推断(真 bge-m3 实测已证伪):
18
- * 切 ids → decode 回文本 → pipeline 再分词,**不保证得到同一序列**——实测 2046 个 id
19
- * 解码后重分词得 2045 token,加 2 个 special token = **2047**,预算 2048 只剩 1 位余量。
20
- * 换一段文本就可能多漂 1 位以上而越界,所以下限靠**实测逐轮收紧**,不靠常数猜测。
17
+ * 为什么不靠「固定留几个位」这种推断(真 bge-m3 实测已证伪):
18
+ * 切 ids → decode 回文本 → pipeline 再分词,**不保证得到同一序列**——在当时的 2048 预算下实测:
19
+ * 切 2046 个 id 解码后重分词得 2045 token,加 2 个 special token = **2047**,**只剩 1 位余量**。
20
+ * 换一段文本就可能多漂一位而越界,所以下限靠**实测逐轮收紧**,不靠常数猜测。
21
21
  *
22
22
  * 不做「短文本跳过 tokenize」的优化:字符→token 比值按词表而变(CJK≈1、emoji/泰文>1.5、
23
23
  * 英文≈0.27),任何启发式下限都可能放走真超限的文本(实测反例:3800 中文字符 ×0.5=1900
@@ -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 后不可
@@ -50,21 +50,29 @@ export declare function catalogById(id: string): CatalogEntry | undefined;
50
50
  /**
51
51
  * 本地 ONNX 推理的单条 token 上限(**唯一硬编码源**,issue #22)。
52
52
  *
53
- * 取值 2048 的理由(不是拍脑袋):#22 实测 Electron 宿主下约 **2700 token** 即被分配器
54
- * SIGTRAP 打死(栈:MatMul 输出张量 → BFCArena::Extend → malloc_zone_memalign),且峰值
55
- * 内存按 **O(T²)** 增长(attention 张量 `[1,heads,T,T]`)——2048 在致死线下留 ≈24% 余量,
56
- * 同时把单次峰值从 ≈4.7GB 压到 ≈0.43 倍。它是**崩溃护栏也是内存护栏**,两者同源。
53
+ * 为什么是 1536 而不是 2048:本值受**连发**致死线约束,不是单发线。
54
+ * #22 报告人在 macOS/M5 上二分实测(Electron-node,5×5000 中文字符串行):
55
+ * 512/1024 全过、1900/1960 全过、**1990/2000/2010/2016/2020/2040/2048 均第 2 条即 SIGTRAP**
56
+ * —— 即**连发致死线落在 1960~1990**,而单发致死线是 ≈2700(issue 初测)。
57
+ * 两者不同源的机理:BFCArena 高水位不回落,第二条进来时第一份 T² 张量尚未回收完,
58
+ * 新块与旧块共存瞬间的峰值才是真正的天花板。
59
+ *
60
+ * 而**真实负载就是连发**:`pipeline/graph.ts` 对 touched 节点「`for … await embed(…)`」
61
+ * 逐条串行,一个投影批里常有 5~8 个节点,其中两条长就命中。
62
+ * 1536 在连发线(≈1960)下留 ≈21% 余量(报告人建议值);代价是单条 infer 从 ~2.2s
63
+ * 降到 ~1.2s,反而使 `embedding.ts` 的 EMA>5s 熔断更不容易被误触发。
64
+ * 它同时是**崩溃护栏与内存护栏**:峰值按 O(T²) 增长(attention 张量 `[1,heads,T,T]`)。
57
65
  *
58
66
  * 不拿 `maxInputChars` 当护栏的原因:bge-m3/embeddinggemma 是 Unigram/BPE,中文≈**1 字/token**
59
67
  * 而 emoji/泰文可 >1.5,所以 5000 字符**既可只 750 token(英文)也可近 8192(中文)**——
60
68
  * 字符与 token 无稳定比值,字符帽永远约不到安全线。
61
69
  */
62
- export declare const HARD_CAP_TOKENS = 2048;
70
+ export declare const HARD_CAP_TOKENS = 1536;
63
71
  /**
64
- * 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-child.cjs`)。
72
+ * 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-token-guard.cjs`)。
65
73
  *
66
74
  * 默认(`userTokens` 缺省/0/负)= `min(modelContext, HARD_CAP_TOKENS)`:bge-small-zh(512)
67
- * 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192) 被压到 2048。`modelContext`
75
+ * 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192) 被压到硬上限。`modelContext`
68
76
  * 取不到(非目录模型)时回退 HARD_CAP_TOKENS,**取小不取大**。
69
77
  *
70
78
  * 用户显式超限只 **clamp 不抛错**(收窄 schema 边界会让已部署 YAML 解析直接失败,故障半径
@@ -78,21 +78,29 @@ export function catalogById(id) {
78
78
  /**
79
79
  * 本地 ONNX 推理的单条 token 上限(**唯一硬编码源**,issue #22)。
80
80
  *
81
- * 取值 2048 的理由(不是拍脑袋):#22 实测 Electron 宿主下约 **2700 token** 即被分配器
82
- * SIGTRAP 打死(栈:MatMul 输出张量 → BFCArena::Extend → malloc_zone_memalign),且峰值
83
- * 内存按 **O(T²)** 增长(attention 张量 `[1,heads,T,T]`)——2048 在致死线下留 ≈24% 余量,
84
- * 同时把单次峰值从 ≈4.7GB 压到 ≈0.43 倍。它是**崩溃护栏也是内存护栏**,两者同源。
81
+ * 为什么是 1536 而不是 2048:本值受**连发**致死线约束,不是单发线。
82
+ * #22 报告人在 macOS/M5 上二分实测(Electron-node,5×5000 中文字符串行):
83
+ * 512/1024 全过、1900/1960 全过、**1990/2000/2010/2016/2020/2040/2048 均第 2 条即 SIGTRAP**
84
+ * —— 即**连发致死线落在 1960~1990**,而单发致死线是 ≈2700(issue 初测)。
85
+ * 两者不同源的机理:BFCArena 高水位不回落,第二条进来时第一份 T² 张量尚未回收完,
86
+ * 新块与旧块共存瞬间的峰值才是真正的天花板。
87
+ *
88
+ * 而**真实负载就是连发**:`pipeline/graph.ts` 对 touched 节点「`for … await embed(…)`」
89
+ * 逐条串行,一个投影批里常有 5~8 个节点,其中两条长就命中。
90
+ * 1536 在连发线(≈1960)下留 ≈21% 余量(报告人建议值);代价是单条 infer 从 ~2.2s
91
+ * 降到 ~1.2s,反而使 `embedding.ts` 的 EMA>5s 熔断更不容易被误触发。
92
+ * 它同时是**崩溃护栏与内存护栏**:峰值按 O(T²) 增长(attention 张量 `[1,heads,T,T]`)。
85
93
  *
86
94
  * 不拿 `maxInputChars` 当护栏的原因:bge-m3/embeddinggemma 是 Unigram/BPE,中文≈**1 字/token**
87
95
  * 而 emoji/泰文可 >1.5,所以 5000 字符**既可只 750 token(英文)也可近 8192(中文)**——
88
96
  * 字符与 token 无稳定比值,字符帽永远约不到安全线。
89
97
  */
90
- export const HARD_CAP_TOKENS = 2048;
98
+ export const HARD_CAP_TOKENS = 1536;
91
99
  /**
92
- * 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-child.cjs`)。
100
+ * 推导生效的本地嵌入 token 预算(**单一实现点**,截断执行在 `resources/embedding-token-guard.cjs`)。
93
101
  *
94
102
  * 默认(`userTokens` 缺省/0/负)= `min(modelContext, HARD_CAP_TOKENS)`:bge-small-zh(512)
95
- * 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192) 被压到 2048。`modelContext`
103
+ * 不受 HARD_CAP 影响(本来就到 512 窗口),bge-m3(8192) 被压到硬上限。`modelContext`
96
104
  * 取不到(非目录模型)时回退 HARD_CAP_TOKENS,**取小不取大**。
97
105
  *
98
106
  * 用户显式超限只 **clamp 不抛错**(收窄 schema 边界会让已部署 YAML 解析直接失败,故障半径
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.26",
4
+ "version": "0.26.28",
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 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.26.26",
3
+ "version": "0.26.28",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",