dsh-prime-memory 0.26.6 → 0.26.10

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,33 @@
1
1
  # 更新日志(Changelog)
2
2
 
3
+ 0.26.10: host enumeration addendum for DSH 0.2.1-rc.3 (declaration-only bump; contract plane zero diff).
4
+
5
+ ## [0.26.9] — 2026-10-23
6
+
7
+ > 本版汇总自 0.26.6 起的「蒸馏分块分载与调度健壮性」工作(计划 distill-load-balance,[ADR-0023](./docs/adr/0023-distill-load-balance-and-secondary-route.md))。0.26.7/0.26.8 仅本地装机、未发布,版本号并入此条。
8
+
9
+ ### 新增
10
+
11
+ - **抽取块级容错 + 部分消费(R1′)**:`runExtraction` 每个抽取块独立 try/catch——单块超时/JSON 崩只丢该块并留待重试、下轮重投,成功块照常进去重+落库。旧行为是任一块崩整批抛穿、整片退回待重试("114 条永不清零"的主因之一)。全块失败仍等价旧"整抛"(不误当零产出成功)。
12
+ - **L2 场景整合分块(R1)**:`runSceneConsolidation` 按字符预算(24k 硬顶)切块逐块整合,单块路径与改前逐字一致(零漂移);多块时跨块合并判断为近似(诚实降级)。治"大库整族 backlog 单调用必超时"。
13
+ - **二线大容量兜底(R3)**:`llm.secondary{provider,model,reasoningEffort,triggerChars}`——单次调用输入 ≥ `triggerChars`(默认 20000)才前置二线,肥调用走能吞大输入的二线、小调用走一线。未配则不启用、绝不静默改选任意模型。
14
+ - **fail-closed 显式路由(R4)**:`llm.requireExplicitRoute`(默认 false 灰度)开启后,一线+二线+回退链皆空 → 抛 `NoDistillRouteError`、蒸馏顺延并告警、**不记退避**,不再蹭宿主 `agentDefaultModel`(区分"没配"与"模型慢")。
15
+ - **路由健康学习(R9,本版已接线)**:`RouteHealthStore`(`distill-route-health.json` sidecar,读 fail-open、只降不升、陈旧过期)记某路由跑通的最小块长,`runExtraction` 读它收紧 perChunk、成功块回记,跨进程免重付退避风暴。
16
+ - **面板可调蒸馏超时(R10)**:设置 → 记忆 → 蒸馏参数 → **蒸馏超时(ms)**(`distillTimeoutMs`,运行时 volatile 覆盖 `llm.timeoutMs`,即时生效)。仅治"慢但未被网关掐"的路由;供应商网关侧固定掐断(如某些免费路由 60s)抬此值无效,须换路由。
17
+ - **整轮抽取墙钟预算 + 模型上限(R5)**:`llm.extractTimeBudgetMs`(默认 0=不限)超预算把剩余块留待重试下轮续,防一个慢模型独占后台串行泵;`llm.modelMaxInputChars`(默认 0=未声明)声明模型真实输入上限,perChunk 取 `min(24k, 本值−42k)`。
18
+ - **反刍豁免退避(R6)/ live flush 队列去重(R7)**:反刍与 rebuild 同为用户显式动作,越过抽取退避;同一 `(session,mode)` 未消费的 force-flush 任务去重,治"一次长阻塞→积压排空刷屏"。
19
+
20
+ ### 变更
21
+
22
+ - **默认输入闸门 `llm.maxInputChars` 700000 → 200000(schema 上限 1000000 → 256000)**:旧默认是远超本地/免费模型窗口的盲值;新默认让 `callLLM` 那道"最后截断闸门"符合本地模型可参与区间。注意 L1 抽取/L2 分块另有 24k 硬顶,不受此值影响;改默认主要防"下一批大调用噎住本地模型",不改变分块大小。
23
+ - **`engines.dsh` 去重**:package.json 尾部误加的重复 `|| 0.2.1-rc.1 || 0.2.1-rc.2` 已删,与 dsh.plugin.json 及各 peer 串逐字同串(跨宿主 6 条 RC 线兼容面不变)。
24
+
25
+ ### 已知限制 / 预留
26
+
27
+ - **去重批容错与 checkpoint 续跑(R1″)未做**:去重阶段某批崩仍是整轮抛穿(整片保留 + 下轮从抽取重跑,无数据丢失但重付抽取成本)。当前治法是为去重/大输入换一条不被网关掐的路由(如二线)。
28
+ - **L3 分块(R2)/ sinceL2 连续失败停攒(R8)未做**:persona 单文档整合分块伤连贯性、收益低于风险;L2 分块后慢性重面积压已缓解。
29
+ - **`llm.chunkShrinkFactor` / `chunkShrinkMaxSteps` / `adaptiveChunk` 为预留配置,当前版本未接线生效**(同轮内超时 ×0.6 缩块尚未实现)——设了不影响行为,请勿依赖。
30
+
3
31
  ## [0.26.6] — 2026-10-22
4
32
 
5
33
  ### 新增
package/README.de.md CHANGED
@@ -61,7 +61,7 @@ alt="DeepSeek Harness Hero-Banner: Unterhaltungen werden automatisch zu mehrschi
61
61
  > Kompatibilitätsmechanismus: Die Settings-Registrierung durchläuft zur Laufzeit drei Zweige
62
62
  > (`register` → `installSection`-Brücke → Dauerein-Notbetrieb), siehe Eintrag 0.11.0 im
63
63
  > [CHANGELOG.md](./CHANGELOG.md). `dsh.plugin.json` deklariert
64
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc).
64
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc).
65
65
 
66
66
  ## Schnellstart
67
67
 
package/README.en.md CHANGED
@@ -61,7 +61,7 @@
61
61
  > Compatibility mechanism: settings registration uses a three-way runtime branch
62
62
  > (`register` → `installSection` bridge → always-on degradation); see the 0.11.0 entry
63
63
  > in [CHANGELOG.md](./CHANGELOG.md). `dsh.plugin.json` declares
64
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc).
64
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc).
65
65
 
66
66
  ## Getting Started
67
67
 
package/README.es.md CHANGED
@@ -61,7 +61,7 @@ alt="Banner hero de DeepSeek Harness: las conversaciones se destilan automática
61
61
  > Mecanismo de compatibilidad: el registro de ajustes pasa en tiempo de ejecución por tres ramas
62
62
  > (`register` → puente `installSection` → degradación a siempre activado),
63
63
  > ver la entrada 0.11.0 del [CHANGELOG.md](./CHANGELOG.md). `dsh.plugin.json` declara
64
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc).
64
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc).
65
65
 
66
66
  ## Inicio rápido
67
67
 
package/README.fr.md CHANGED
@@ -61,7 +61,7 @@ alt="Bannière hero de DeepSeek Harness : les conversations sont automatiquement
61
61
  > Mécanisme de compatibilité : l'enregistrement settings passe par trois branches à l'exécution
62
62
  > (`register` → pont `installSection` → dégradation en mode toujours actif),
63
63
  > voir l'entrée 0.11.0 du [CHANGELOG.md](./CHANGELOG.md). `dsh.plugin.json` déclare
64
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc).
64
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc).
65
65
 
66
66
  ## Démarrage rapide
67
67
 
package/README.it.md CHANGED
@@ -61,7 +61,7 @@ alt="Banner hero di DeepSeek Harness: le conversazioni vengono distillate automa
61
61
  > Meccanismo di compatibilità: la registrazione delle impostazioni passa per tre rami a runtime
62
62
  > (`register` → ponte `installSection` → degrado a sempre-attivo),
63
63
  > vedi la voce 0.11.0 del [CHANGELOG.md](./CHANGELOG.md). `dsh.plugin.json` dichiara
64
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc).
64
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc).
65
65
 
66
66
  ## Avvio rapido
67
67
 
package/README.ja.md CHANGED
@@ -62,7 +62,7 @@ alt="DeepSeek Harness ヒーロー画像:会話がバックグラウンドで
62
62
 
63
63
  > 互換メカニズム:settings 登録は 3 分岐のランタイム判定(`register` → `installSection` ブリッジ → 常時オン縮退)。
64
64
  > 詳細は [CHANGELOG.md](./CHANGELOG.md) の 0.11.0 エントリ参照。`dsh.plugin.json` は
65
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc) を宣言。
65
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc) を宣言。
66
66
 
67
67
  ## クイックスタート
68
68
 
package/README.ko.md CHANGED
@@ -62,7 +62,7 @@ alt="DeepSeek Harness 히어로 이미지: 대화가 백그라운드에서 계
62
62
 
63
63
  > 호환 메커니즘: settings 등록은 3분기 런타임 분기(`register` → `installSection` 브리지 → 상시 온 강등).
64
64
  > 자세한 내용은 [CHANGELOG.md](./CHANGELOG.md)의 0.11.0 항목 참조. `dsh.plugin.json`은
65
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc) 선언.
65
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc) 선언.
66
66
 
67
67
  ## 빠른 시작
68
68
 
package/README.md CHANGED
@@ -60,7 +60,7 @@ alt="DeepSeek Harness hero 横幅:对话自动分层蒸馏成记忆,模型
60
60
 
61
61
  > 兼容机制:settings 注册走运行时三分支(`register` → `installSection` 桥接 → 恒开降级),
62
62
  > 详见 [CHANGELOG.md](./CHANGELOG.md) 的 0.11.0 条目。`dsh.plugin.json` 声明
63
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc)。
63
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc)。
64
64
 
65
65
  ## 快速开始
66
66
 
@@ -352,8 +352,13 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
352
352
  | `llm.maxTokens` | `65536` | 未分层调用的兜底输出总闸。各蒸馏层有独立预算(抽取 16k / 去重 8k / L2 32k / L3 16k;思考档 high/xhigh/max 时自动 ×4,防 reasoning 吃光预算),分层预算可在设置页 → 记忆 → 概览 → 蒸馏参数运行时调整(留空/0 = 跟随内置默认) |
353
353
  | `llm.reasoningEffort` | 空 | 蒸馏思考档位:空串 = **自动**(按模型能力解析:模型默认档 → `high`);显式值(`off`/`none`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max`)仅在该模型声明支持时发送——跨供应商 effort 词汇表不同(deepseek 认 `off`,OpenAI 系是 `none`,未声明档位的模型不传),不支持的档位自动降级为不传并告警一次;思考档 high/xhigh/max 时输出预算自动 ×4。运行时可在设置页路由链编辑器里**逐路由**覆盖档位(行内下拉,词表按各模型声明的能力实时显示,缺省跟随本值) |
354
354
  | `llm.temperature` | `0.3` | 蒸馏温度 |
355
- | `llm.maxInputChars` | `700000` | 单次蒸馏输入字符预算(超限的 L1 输入自动分块抽取);运行时可在设置页 → 蒸馏参数 → 输入预算调整(留空/0 = 跟随本值) |
356
- | `llm.timeoutMs` | `120000` | 单次蒸馏调用超时(ms) |
355
+ | `llm.maxInputChars` | `200000` | 单次蒸馏调用输入闸门(超限截断);默认 200k 适配本地/免费模型窗口,上限 256k,可再往下调。L1 抽取/L2 另有 24k 分块硬顶,不受此值影响;运行时可在设置页 → 蒸馏参数 → 输入预算调整(留空/0 = 跟随本值) |
356
+ | `llm.timeoutMs` | `120000` | 单次蒸馏调用超时(ms);运行时可在设置页 → 蒸馏参数 → 蒸馏超时(ms) 覆盖(仅治"慢但未被网关掐"的路由,供应商网关侧固定掐断抬此值无效) |
357
+ | `llm.extractTimeBudgetMs` | `0` | 整轮抽取墙钟预算(ms):`0` = 不限(旧行为);`>0` 时多块抽取累计超此预算即把剩余块留待重试、下轮续跑,防一个慢模型独占后台串行泵([ADR-0023](./docs/adr/0023-distill-load-balance-and-secondary-route.md)) |
358
+ | `llm.modelMaxInputChars` | `0` | 蒸馏模型真实输入上限(字符):`0` = 未声明(回退保守口径 `min(24k, maxInputChars−42k)`);声明后抽取/L2 块大小取 `min(24k, 本值−42k)`,避免默认值撞本地模型硬限 |
359
+ | `llm.secondary` | 关 | 二线大容量兜底 `{provider,model,reasoningEffort,triggerChars}`:单次调用输入 ≥ `triggerChars`(默认 20000)才前置二线(肥调用走能吞大输入的二线、小调用走一线);未配则不启用、绝不静默改选任意模型 |
360
+ | `llm.requireExplicitRoute` | `false` | 显式路由闸(灰度):`true` 时一线+二线+回退链皆空 → 蒸馏顺延并告警(`NoDistillRouteError`、不记退避),不再蹭宿主默认模型;`false` 保持旧行为 |
361
+ | `llm.chunkShrink*` 等(预留) | — | ⚠ `llm.chunkShrinkFactor` / `chunkShrinkMaxSteps` / `adaptiveChunk` 为**预留配置,当前版本未接线生效**(同轮内超时 ×0.6 缩块尚未实现)——设了不影响行为,请勿依赖 |
357
362
  | `tokenCost.retentionDays` | `365` | 蒸馏成本明细(token\_cost 表)保留天数,写入时滚动清理更早行;`0` = 永久保留。成本看板「近 N 天」窗口上限同此值 |
358
363
  | `tools` | `true` | 是否注册模型可调用的记忆工具 |
359
364
  | `benchControl` | `false` | 注册 bench 控制服务(进程内 rebuild 触发/会话档位设置/蒸馏用量快照,供基准 lifecycle 赛道)。默认关——生产部署零表面积,勿随意开启 |
@@ -415,6 +420,14 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
415
420
 
416
421
  4. **调高超时**:`llm.timeoutMs` 只在路由确实慢但网关不掐时有用;网关 20s 掐断的场景调插件超时无效,请用前两层。
417
422
 
423
+ ### 二线大容量兜底与超时可调(v0.26.7+)
424
+
425
+ - **面板可调超时**:设置 → 记忆 → 蒸馏参数 → **蒸馏超时(ms)**(`distillTimeoutMs`,即时生效、无需重启)。只救"慢但未被网关掐"的路由;供应商网关侧固定掐断(如某些免费路由 60s)抬此值无效,须换路由。
426
+ - **二线模型(大容量兜底)**:`llm.secondary{provider,model,triggerChars}`——单次调用输入 ≥ `triggerChars` 才走二线(小调用走便宜快的一线、肥调用走能吞大输入的二线)。未配则不启用、绝不静默改选任意模型。
427
+ - **分块分载与容错**:L1 抽取 / L2 场景整合按字符预算分块,单块超时/崩只丢该块、留待重试,不再"一块崩整批丢"(待重试不再被整片回滚)。`llm.extractTimeBudgetMs`(默认 0=不限)可给整轮抽取设墙钟预算,防一个慢模型独占后台;`llm.modelMaxInputChars` 声明模型真实输入上限,块大小据此收紧。
428
+ - **显式路由(灰度)**:`llm.requireExplicitRoute=true` 时,未显式配一线/二线/回退链 → 蒸馏顺延并告警,不再蹭宿主默认模型(默认 false 保持旧行为)。
429
+ - **预留未生效的配置**:`llm.chunkShrinkFactor` / `llm.chunkShrinkMaxSteps` / `llm.adaptiveChunk` 已在 schema 声明但**尚未接线**(同轮内超时自动缩块未实现)——设了不影响行为;跨轮自适应已由 R9 路由健康学习承担。
430
+
418
431
  ## 日志与故障排查
419
432
 
420
433
  dsh 宿主把插件日志打到控制台;插件另把 info 级以上镜像到数据目录的 `memory.log`。
package/README.ru.md CHANGED
@@ -61,7 +61,7 @@ alt="Баннер hero DeepSeek Harness: диалоги автоматическ
61
61
  > Механизм совместимости: регистрация настроек на этапе исполнения идёт по трём ветвям
62
62
  > (`register` → мост `installSection` → деградация в режим «всегда включено»),
63
63
  > подробнее — запись 0.11.0 в [CHANGELOG.md](./CHANGELOG.md). `dsh.plugin.json` объявляет
64
- > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.2,17 rc).
64
+ > `engines.dsh` 为逐 rc 精确枚举串(0.1.0-rc.2 → 0.2.1-rc.3,18 rc).
65
65
 
66
66
  ## Быстрый старт
67
67
 
package/dist/client.js CHANGED
@@ -37811,9 +37811,12 @@ var __create = Object.create;
37811
37811
  const ib = data.inputBudget || { current: 0, fallback: 0, effective: 0 };
37812
37812
  const curIn = ib.current || 0;
37813
37813
  const effIn = ib.effective || ib.fallback || 0;
37814
+ const tb = data.timeoutBudget || { current: 0, fallback: 0, effective: 0 };
37815
+ const curTo = tb.current || 0;
37816
+ const effTo = tb.effective || tb.fallback || 0;
37814
37817
  const shown = (key) => {
37815
37818
  if (draft && draft[key] !== void 0) return draft[key];
37816
- const c = key === "input" ? curIn : cur[key] || 0;
37819
+ const c = key === "input" ? curIn : key === "timeout" ? curTo : cur[key] || 0;
37817
37820
  return c > 0 ? String(c) : "";
37818
37821
  };
37819
37822
  const commitPart = (keys, buildPayload, applyView) => {
@@ -37823,11 +37826,11 @@ var __create = Object.create;
37823
37826
  const key = keys[i];
37824
37827
  const raw = (draft[key] !== void 0 ? draft[key] : shown(key)).trim();
37825
37828
  const n = raw === "" ? 0 : Number(raw);
37826
- const max = key === "input" ? 1e6 : 1e6;
37827
- const min = key === "input" ? raw === "" ? 0 : 1e3 : 0;
37829
+ const max = key === "timeout" ? 6e5 : key === "input" ? 256e3 : 1e6;
37830
+ const min = key === "input" || key === "timeout" ? raw === "" ? 0 : 1e3 : 0;
37828
37831
  if (!Number.isInteger(n) || n < min || n > max) {
37829
37832
  onError(
37830
- key === "input" ? "输入预算须为 0 或 1000~1000000 的整数(留空或 0 = 跟随配置)" : "输出预算须为 0~1000000 的整数(留空或 0 = 跟随默认)"
37833
+ key === "input" ? "输入预算须为 0 或 1000~256000 的整数(留空或 0 = 跟随配置)" : key === "timeout" ? "蒸馏超时须为 0 或 1000~600000 的整数(留空或 0 = 跟随配置)" : "输出预算须为 0~1000000 的整数(留空或 0 = 跟随默认)"
37831
37834
  );
37832
37835
  setDraft(null);
37833
37836
  return;
@@ -37888,12 +37891,27 @@ var __create = Object.create;
37888
37891
  }
37889
37892
  );
37890
37893
  };
37894
+ const commitTimeout = () => {
37895
+ commitPart(
37896
+ ["timeout"],
37897
+ (v) => ({ distillTimeoutMs: v.timeout }),
37898
+ (prev, v) => ({
37899
+ ...prev,
37900
+ settings: { ...prev.settings, distillTimeoutMs: v.timeout },
37901
+ timeoutBudget: {
37902
+ ...prev.timeoutBudget || { current: 0, fallback: 0, effective: 0 },
37903
+ current: v.timeout,
37904
+ effective: v.timeout > 0 ? v.timeout : tb.fallback || 0
37905
+ }
37906
+ })
37907
+ );
37908
+ };
37891
37909
  const dirty = (keys) => {
37892
37910
  if (!draft) return false;
37893
37911
  for (let i = 0; i < keys.length; i++) {
37894
37912
  const key = keys[i];
37895
37913
  const want = (draft[key] !== void 0 ? draft[key] : "").trim();
37896
- const was = key === "input" ? curIn > 0 ? String(curIn) : "" : (cur[key] || 0) > 0 ? String(cur[key]) : "";
37914
+ const was = key === "input" ? curIn > 0 ? String(curIn) : "" : key === "timeout" ? curTo > 0 ? String(curTo) : "" : (cur[key] || 0) > 0 ? String(cur[key]) : "";
37897
37915
  if (want !== was) return true;
37898
37916
  }
37899
37917
  return false;
@@ -37975,6 +37993,28 @@ var __create = Object.create;
37975
37993
  ),
37976
37994
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: "字符" })
37977
37995
  ] }) })
37996
+ ] }) : null,
37997
+ showInput ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { marginTop: 12 }, children: [
37998
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
37999
+ "div",
38000
+ {
38001
+ style: S.switchLabel,
38002
+ title: '单次蒸馏调用超时(ms):治"慢但未被网关掐"的路由需更大超时窗;留空或 0 = 跟随配置(当前生效 ' + (effTo || "?") + "ms,来自 llm.timeoutMs)。注:供应商网关侧固定掐断(如某些路由 60s)抬此值无效,须换路由",
38003
+ children: "蒸馏超时"
38004
+ }
38005
+ ),
38006
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
38007
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { width: 68, flexShrink: 0, fontSize: 12, color: "var(--dsh-mem-text-2)" }, children: "超时" }),
38008
+ inputBox(
38009
+ "timeout",
38010
+ "超时",
38011
+ "单次蒸馏调用超时(ms)(留空 = 跟随配置 " + (tb.fallback || "?") + ")",
38012
+ 110,
38013
+ tb.fallback,
38014
+ commitTimeout
38015
+ ),
38016
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: "ms" })
38017
+ ] }) })
37978
38018
  ] }) : null
37979
38019
  ] });
37980
38020
  }
package/dist/config.d.ts CHANGED
@@ -61,6 +61,10 @@ export interface MemoryConfig {
61
61
  backgroundMessages: number;
62
62
  /** 去重候选池大小(每条新记忆的相似候选数)。 */
63
63
  candidatePool: number;
64
+ /** L1 去重批连续失败上限:达此值则 fail-loud + park,不再无限重放同肥调用。R1″。 */
65
+ dedupRetryCap?: number;
66
+ /** 抽取结果 checkpoint 落盘:去重/落库失败后下轮从去重续跑、不重抽已成功块。R1″。 */
67
+ extractCheckpoint?: boolean;
64
68
  };
65
69
  l2: {
66
70
  enabled: boolean;
@@ -70,6 +74,8 @@ export interface MemoryConfig {
70
74
  maxScenes: number;
71
75
  /** L2 prompt 里附带的相似场景全文数量上限。 */
72
76
  sceneContextLimit: number;
77
+ /** L2 场景整合连续失败上限:达此值停攒该族 + fail-loud,避免同肥调用每轮重放。R8。 */
78
+ consecutiveFailCap?: number;
73
79
  };
74
80
  l3: {
75
81
  enabled: boolean;
@@ -390,6 +396,31 @@ export interface MemoryConfig {
390
396
  maxInputChars: number;
391
397
  /** 单次蒸馏调用超时(ms)。 */
392
398
  timeoutMs: number;
399
+ /** R5 整轮抽取墙钟预算(ms):多块抽取累计超此预算即把剩余块留 pending 下轮续,
400
+ * 防一个慢模型独占串行泵十几分钟。0 = 不限(旧行为)。 */
401
+ extractTimeBudgetMs?: number;
402
+ /** 二线蒸馏模型(大容量兜底):仅当单次调用输入字符 ≥ triggerChars 才启用;
403
+ * provider/model 任一为空 = 该档不可用(不隐式改选默认模型)。R3。 */
404
+ secondary?: {
405
+ provider: string;
406
+ model: string;
407
+ reasoningEffort?: string;
408
+ triggerChars: number;
409
+ };
410
+ /** 蒸馏模型真实输入上限(字符):perChunk 初值天花板;0/未声明 = 回退旧口径
411
+ * min(24k, maxInputChars-42k)。R5/R11。 */
412
+ modelMaxInputChars?: number;
413
+ /** 自适应分块:超时/报错后块大小按此因子缩减(默认 0.6=减四成)。R5。 */
414
+ chunkShrinkFactor?: number;
415
+ /** 块缩减最大重试步数(触底=单条或达此步数即止)。R5。 */
416
+ chunkShrinkMaxSteps?: number;
417
+ /** 自适应分块总开关:关=固定 perChunk(零漂移)。R5。 */
418
+ adaptiveChunk?: {
419
+ enabled: boolean;
420
+ };
421
+ /** 蒸馏须显式配路由:true 时一线+二线+链皆空 → 抛 NoDistillRouteError(不蹭默认模型)。
422
+ * 灰度默认 false(不破坏"留空跟默认模型"的存量用户)。R4。 */
423
+ requireExplicitRoute?: boolean;
393
424
  /** 分层输出预算运行时覆盖(设置页 distillBudgets 经 effectiveCfg 注入;
394
425
  * 0/缺省 = 用内置默认。非静态 schema——预算无部署上限语义)。 */
395
426
  budgets?: Partial<{
package/dist/config.js CHANGED
@@ -54,7 +54,9 @@ export function liveSettingsSchema() {
54
54
  // 图谱投影输出预算(投影 job 单批 ≤8 条记录,默认 8000)
55
55
  graph: budget(),
56
56
  }).default({ extract: 0, dedup: 0, l2: 0, l3: 0, graph: 0 }),
57
- distillMaxInputChars: Schema.number().min(0).max(1_000_000).default(0),
57
+ distillMaxInputChars: Schema.number().min(0).max(256_000).default(0),
58
+ // R10 蒸馏超时运行时覆盖(ms):0 = 跟随静态 llm.timeoutMs;上限与部署 schema 同(600s)
59
+ distillTimeoutMs: Schema.number().min(0).max(600_000).default(0),
58
60
  // 蒸馏通道运行时覆盖:'' = 跟随部署 config / 'host' = 复用宿主 / 'direct' = 原生直连
59
61
  distillMode: Schema.union(['', 'host', 'direct']).default(''),
60
62
  directBaseURL: Schema.string().default(''),
@@ -144,12 +146,15 @@ export const memorySchema = Schema.object({
144
146
  idleSeconds: Schema.number().min(0).max(86_400).default(300),
145
147
  backgroundMessages: Schema.number().min(0).max(50).default(10),
146
148
  candidatePool: Schema.number().min(1).max(20).default(5),
149
+ dedupRetryCap: Schema.number().min(1).max(20).default(5),
150
+ extractCheckpoint: Schema.boolean().default(true),
147
151
  }),
148
152
  l2: Schema.object({
149
153
  enabled: Schema.boolean().default(true),
150
154
  minNewMemories: Schema.number().min(1).max(100).default(5),
151
155
  maxScenes: Schema.number().min(1).max(100).default(12),
152
156
  sceneContextLimit: Schema.number().min(0).max(20).default(3),
157
+ consecutiveFailCap: Schema.number().min(1).max(20).default(3),
153
158
  }),
154
159
  l3: Schema.object({
155
160
  enabled: Schema.boolean().default(true),
@@ -361,9 +366,26 @@ export const memorySchema = Schema.object({
361
366
  // 旧默认 'off' 在非 deepseek 模型上必炸(400/本地拒绝)
362
367
  reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
363
368
  temperature: Schema.number().min(0).max(2).default(0.3),
364
- // 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
365
- maxInputChars: Schema.number().min(1000).max(1_000_000).default(700_000),
369
+ // 单次调用输入闸门默认压到本地模型可参与区间(200k,可再降);上限 256k(<256 KiB)。
370
+ // 旧 700k 为盲值、远超本地/免费路由窗口 → 大调用撞模型硬限。抽取/L2 分块另有 24k 硬顶,不受此影响。
371
+ maxInputChars: Schema.number().min(1000).max(256_000).default(200_000),
366
372
  timeoutMs: Schema.number().min(1000).max(600_000).default(120_000),
373
+ // R5 整轮抽取墙钟预算(ms):0=不限(旧行为);>0 超预算把剩余块留 pending 下轮续
374
+ extractTimeBudgetMs: Schema.number().min(0).max(3_600_000).default(0),
375
+ // 二线蒸馏模型(大容量兜底):输入 ≥ triggerChars 才启用;provider/model 空=该档不可用
376
+ secondary: Schema.object({
377
+ provider: Schema.string().default(''),
378
+ model: Schema.string().default(''),
379
+ reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
380
+ triggerChars: Schema.number().min(1000).max(1_000_000).default(20_000),
381
+ }).default({ provider: '', model: '', reasoningEffort: '', triggerChars: 20_000 }),
382
+ // 蒸馏模型真实输入上限(0=未声明,回退旧口径 min(24k, maxInputChars-42k))
383
+ modelMaxInputChars: Schema.number().min(0).max(1_000_000).default(0),
384
+ chunkShrinkFactor: Schema.number().min(0.1).max(0.95).default(0.6),
385
+ chunkShrinkMaxSteps: Schema.number().min(0).max(8).default(4),
386
+ adaptiveChunk: Schema.object({ enabled: Schema.boolean().default(true) }).default({ enabled: true }),
387
+ // 灰度默认 false:翻 true 后蒸馏须显式配路由(不蹭宿主默认模型),配 NoDistillRouteError
388
+ requireExplicitRoute: Schema.boolean().default(false),
367
389
  }),
368
390
  hall: Schema.object({
369
391
  enabled: Schema.array(Schema.string()).default([...WING_DEFAULT_ENABLED]),
@@ -77,7 +77,10 @@ export interface MemoryLiveSettings {
77
77
  /** 输入预算运行时覆盖(字符,≈token):单次蒸馏调用的输入上限,L1 按此分块、
78
78
  * 超限截断;0 = 跟随静态配置 llm.maxInputChars。 */
79
79
  distillMaxInputChars: number;
80
- /** 运行时按层路由链:层键 l1/l2/l3 各一条完整链;非空即完整接管该层解析
80
+ /** R10 蒸馏调用超时运行时覆盖(ms):0 = 跟随静态配置 llm.timeoutMs;>0 = 覆盖。
81
+ * 治"慢但未被网关掐"的路由需更大超时窗(注:供应商网关侧固定掐断抬此值无效,须换路由)。 */
82
+ distillTimeoutMs: number;
83
+ /** 运行时按层路由链:层键 l1/l2/l3 各一条完整链;非空即完整接管该层解析
81
84
  * (压过静态 layerRoutes,层内第一优先级);空数组 = 该层跟随(静态层链 → 全局解析逐级兜底)。
82
85
  * 写入经 settings-set 逐层校验(头行必须显式)。 */
83
86
  distillLayerChains: Record<LayerRouteKey, DistillChainEntry[]>;
@@ -805,6 +808,12 @@ export interface SettingsGetResponse {
805
808
  fallback: number;
806
809
  effective: number;
807
810
  };
811
+ /** R10 蒸馏超时(ms):current 运行时覆盖(0=跟随) / fallback 静态 llm.timeoutMs / effective 生效值。 */
812
+ timeoutBudget: {
813
+ current: number;
814
+ fallback: number;
815
+ effective: number;
816
+ };
808
817
  }
809
818
  /** dsh-memory/settings-set(patch 语义:只带要改的键)。 */
810
819
  export interface SettingsSetRequest {
@@ -826,6 +835,8 @@ export interface SettingsSetRequest {
826
835
  graph: number;
827
836
  };
828
837
  distillMaxInputChars?: number;
838
+ /** R10 蒸馏超时运行时覆盖(ms):0 = 跟随配置。 */
839
+ distillTimeoutMs?: number;
829
840
  /** md 投射/后缀收敛的单次动作护栏覆盖:0 = 跟随部署 `cfg.memExport.maxRecords`。 */
830
841
  mdExportMaxRecords?: number;
831
842
  /** 蒸馏通道运行时覆盖:'' = 跟随部署 config;'host' = 复用宿主;'direct' = 插件原生直连。 */
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ import { SceneStore } from './store/scenes.js';
34
34
  import { SessionModeStore } from './store/session-modes.js';
35
35
  import { StateStore } from './store/state.js';
36
36
  import { SlotStore } from './store/slots.js';
37
+ import { RouteHealthStore, routeHealthPathFor } from './store/route-health.js';
37
38
  import { registerMemoryTools } from './tools/index.js';
38
39
  import { registerSlotTools } from './tools/slots.js';
39
40
  import { registerSlotRecall } from './hooks/slot-recall.js';
@@ -207,6 +208,7 @@ export async function apply(ctx, config) {
207
208
  };
208
209
  const stores = {
209
210
  usage: undefined, // 装配于下方(依赖 dataDir 就绪)
211
+ routeHealth: undefined, // R9 路由健康学习,装配于下方
210
212
  // DRSC 客户端(T8.4 读切):蒸馏背景窗 + conversation_search 工具面的历史供给。
211
213
  // 硬依赖无本地回退(l0 表退役);trustedHosts 惰性读取(webStartup 可能晚到)。
212
214
  drsc: new DrscClient({ trustedHosts: () => readStartupTrustedHosts(ctx) }),
@@ -434,6 +436,10 @@ export async function apply(ctx, config) {
434
436
  const recallUsage = new RecallUsageStore(dataDir, logger);
435
437
  await recallUsage.init();
436
438
  stores.usage = recallUsage;
439
+ // R9 路由健康学习:跨进程留存"退避后跑通的最小块长",供 runExtraction 收紧 perChunk。
440
+ const routeHealth = new RouteHealthStore(routeHealthPathFor(dataDir));
441
+ await routeHealth.load();
442
+ stores.routeHealth = routeHealth;
437
443
  // 召回相关度反馈(人在召回卡上打 -1..5)与「已声明过时」待播报队列。
438
444
  // 两个都是低频用户动作 → 立即落盘;sidecar 缺席/损坏只降级为只读,不阻断任何读路径。
439
445
  const recallFeedback = new RecallFeedbackStore(dataDir, logger);
package/dist/llm.d.ts CHANGED
@@ -71,11 +71,25 @@ export declare const HIGH_EFFORT_TIERS: readonly ["high", "xhigh", "max"];
71
71
  * 预算致正文 0 字符)——effort 为 high/xhigh/max 时分层预算 ×4。
72
72
  */
73
73
  export declare function layerMaxTokens(base: number, reasoningEffort: string): number;
74
+ /** 无可用蒸馏路由(显式一线/二线/链皆缺且 requireExplicitRoute=true):
75
+ * runner 据此 skip+顺延、不记退避——区别于"模型慢/超时"(那才退避)。R4。 */
76
+ export declare class NoDistillRouteError extends Error {
77
+ constructor(msg?: string);
78
+ }
74
79
  /** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
75
80
  export declare function resolveModelRoute(ctx: Context, cfg: MemoryConfig): Promise<{
76
81
  provider: string;
77
82
  model: string;
78
83
  }>;
84
+ /**
85
+ * 二线(大容量兜底)选路(R3):单次调用输入字符 ≥ triggerChars 且二线已配 → 返回二线路由,
86
+ * 否则 null(走一线/链)。二线未配(provider/model 任一空)→ 永不命中,不隐式改选任意模型。
87
+ */
88
+ export declare function pickSecondary(cfg: MemoryConfig, inputChars: number): {
89
+ provider: string;
90
+ model: string;
91
+ reasoningEffort?: string;
92
+ } | null;
79
93
  /** 回退链条目(配置形态;reasoningEffort 为该路由的档位覆盖,'' = 跟随全局)。 */
80
94
  export interface FallbackRouteEntry {
81
95
  provider: string;
package/dist/llm.js CHANGED
@@ -46,11 +46,28 @@ export const HIGH_EFFORT_TIERS = ['high', 'xhigh', 'max'];
46
46
  export function layerMaxTokens(base, reasoningEffort) {
47
47
  return HIGH_EFFORT_TIERS.includes(reasoningEffort) ? base * 4 : base;
48
48
  }
49
+ /** 无可用蒸馏路由(显式一线/二线/链皆缺且 requireExplicitRoute=true):
50
+ * runner 据此 skip+顺延、不记退避——区别于"模型慢/超时"(那才退避)。R4。 */
51
+ export class NoDistillRouteError extends Error {
52
+ constructor(msg = '无可用蒸馏路由:已开启 llm.requireExplicitRoute,但未显式配置一线/二线/回退链') {
53
+ super(msg);
54
+ this.name = 'NoDistillRouteError';
55
+ }
56
+ }
49
57
  /** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
50
58
  export async function resolveModelRoute(ctx, cfg) {
51
59
  if (cfg.llm.provider && cfg.llm.model) {
52
60
  return { provider: cfg.llm.provider, model: cfg.llm.model };
53
61
  }
62
+ // R4 fail-closed:开了 requireExplicitRoute 且二线/回退链都未配 → 不蹭宿主默认模型,
63
+ // 抛专用错误(runner 侧 skip+顺延、不记退避——"没配路由"≠"模型慢")。关时逐字走旧路径。
64
+ if (cfg.llm.requireExplicitRoute === true) {
65
+ const sec = cfg.llm.secondary;
66
+ const hasSecondary = !!(sec && sec.provider && sec.model);
67
+ const hasChain = (cfg.llm.fallbacks?.length ?? 0) > 0;
68
+ if (!hasSecondary && !hasChain)
69
+ throw new NoDistillRouteError();
70
+ }
54
71
  // agentDefaultModel 是可选服务,缺失时不得抛错。
55
72
  const defaults = ctx.get('agentDefaultModel');
56
73
  const sel = defaults?.currentSelection?.();
@@ -59,6 +76,19 @@ export async function resolveModelRoute(ctx, cfg) {
59
76
  }
60
77
  throw new Error('无法解析蒸馏模型路由:请在插件 config 中配置 llm.provider / llm.model,或确保存在默认模型选择');
61
78
  }
79
+ /**
80
+ * 二线(大容量兜底)选路(R3):单次调用输入字符 ≥ triggerChars 且二线已配 → 返回二线路由,
81
+ * 否则 null(走一线/链)。二线未配(provider/model 任一空)→ 永不命中,不隐式改选任意模型。
82
+ */
83
+ export function pickSecondary(cfg, inputChars) {
84
+ const sec = cfg.llm.secondary;
85
+ if (!sec || !sec.provider || !sec.model)
86
+ return null;
87
+ const trigger = sec.triggerChars > 0 ? sec.triggerChars : Number.POSITIVE_INFINITY;
88
+ if (!(inputChars >= trigger))
89
+ return null;
90
+ return { provider: sec.provider, model: sec.model, ...(sec.reasoningEffort ? { reasoningEffort: sec.reasoningEffort } : {}) };
91
+ }
62
92
  /**
63
93
  * 组装蒸馏路由链(纯决策,决策表缝):主路由在前、回退条目按配置顺序在后。
64
94
  * provider/model 缺失的条目剔除;与主路由或先前条目完全相同(provider+model)的
@@ -370,8 +400,17 @@ export async function callLLM(ctx, cfg, opts) {
370
400
  // 宿主路由链:opts.layer 配了层链的层走层链完整替换,其余走全局解析;
371
401
  // direct 模式下这一段充当付费/默认模型的兜底安全网。
372
402
  const routes = await resolveLayerRoutes(ctx, cfg, opts.layer);
373
- for (let i = 0; i < routes.length; i++) {
374
- const route = routes[i];
403
+ // R3 二线(大容量兜底):输入 ≥ triggerChars 且二线已配 → 前置二线,让肥调用先走大容量档;
404
+ // 二线未配时 sec=null → 链逐字不变(零漂移)。已在链里的同路由去重。
405
+ const sec = pickSecondary(cfg, opts.user.length);
406
+ const chain = sec
407
+ ? [
408
+ { provider: sec.provider, model: sec.model, effort: sec.reasoningEffort ?? '' },
409
+ ...routes.filter((r) => !(r.provider === sec.provider && r.model === sec.model)),
410
+ ]
411
+ : routes;
412
+ for (let i = 0; i < chain.length; i++) {
413
+ const route = chain[i];
375
414
  try {
376
415
  return await callRoute(ctx, cfg, opts, route);
377
416
  }
@@ -381,7 +420,7 @@ export async function callLLM(ctx, cfg, opts) {
381
420
  throw err;
382
421
  lastErr = err;
383
422
  warnRouteDeadOnce(route, opts.logger);
384
- const next = routes[i + 1];
423
+ const next = chain[i + 1];
385
424
  if (next) {
386
425
  opts.logger?.info(`[memory] 蒸馏路由降级 ${route.provider}/${route.model} → ${next.provider}/${next.model}(${errDetail(err)})`);
387
426
  }
@@ -1,8 +1,15 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { MemoryConfig } from '../config.js';
3
+ import type { RouteHealthStore } from '../store/route-health.js';
3
4
  import type { L1Store } from '../store/l1.js';
4
5
  import type { MemoryState } from '../store/state.js';
5
6
  import type { ConversationAnchor, ConversationMessage, ExtractedMemory, ExtractMode, MemoryFamily, MemoryLogger, MemoryRecord, MemoryScope } from '../types.js';
7
+ /**
8
+ * L2 场景整合单块字符硬顶(R1):与抽取同精神——旧版一次把整族 backlog 塞单调用,
9
+ * 库越大越必撞超时(实测 work-L2 >60s abort)。砍到 24k 后逐块整合,失败半径从
10
+ * “整族 backlog”缩到“一块”。默认对齐 MAX_EXTRACT_CHUNK_CHARS。
11
+ */
12
+ export declare const MAX_SCENE_CHUNK_CHARS = 24000;
6
13
  export interface ExtractionResult {
7
14
  stored: number;
8
15
  skipped: boolean;
@@ -12,6 +19,8 @@ export interface ExtractionResult {
12
19
  runId?: string;
13
20
  /** §F 追踪捎带:六值决策词表聚合;早退分支缺省。 */
14
21
  byKind?: Record<string, number>;
22
+ /** R1′ 抽取块级失败的消息:调用方应留桶下轮重投(部分消费,不整批回滚)。 */
23
+ failedMessages?: ConversationMessage[];
15
24
  }
16
25
  /** 分族 checkpoint 桶(活引用,改动由调用方 save 落盘)。 */
17
26
  export type FamilyStates = Record<MemoryFamily, MemoryState>;
@@ -55,7 +64,9 @@ export declare function toStoreRecord(m: PendingMemory, now: number, ts: number,
55
64
  * 按字符预算把消息切成多块(保持顺序,单条超预算独占一块,由 callLLM 兜底截断)。
56
65
  * 每条消息按 content 长度 + 64 字符脚手架开销(id/时间戳行)计。
57
66
  */
58
- export declare function chunkByCharBudget(messages: ConversationMessage[], budgetChars: number): ConversationMessage[][];
67
+ export declare function chunkByCharBudget<T extends {
68
+ content: string;
69
+ }>(messages: readonly T[], budgetChars: number): T[][];
59
70
  export declare function runExtraction(ctx: Context, cfg: MemoryConfig, store: L1Store, states: FamilyStates, pending: ConversationMessage[], background: ConversationMessage[], logger: MemoryLogger, mode: ExtractMode,
60
71
  /**
61
72
  * §E 当前工作区标识(由调用方经 `sessionWorkspaceIdOf` 解析;拿不到传 undefined)。
@@ -74,4 +85,6 @@ anchorMap?: ReadonlyMap<string, ConversationAnchor>,
74
85
  * 另两个 sidecar 里,而 merge 每次新铸 id——不搬就等于每次合并把这条记忆的
75
86
  * `used`/人工打分清零,它会被当成“没用过”而沉底。缺省不搬(行为与改动前一致)。
76
87
  */
77
- mergeRekey?: (oldId: string, newId: string) => void): Promise<ExtractionResult>;
88
+ mergeRekey?: (oldId: string, newId: string) => void,
89
+ /** R9 路由健康学习(可选):读已学到的跑通块长收紧 perChunk,成功块回记。未传=不启用。 */
90
+ routeHealth?: RouteHealthStore): Promise<ExtractionResult>;